FastAPI 프로젝트 고유 설정 및 패턴 레퍼런스. Use when: 프로젝트 구조 잡기, 폴더 구조, 디렉토리 구조, 초기 세팅, 새 프로젝트 시작, 파일 분리, 파일이 너무 커, 폴더로 변환, 파일 비대, 모듈 분리, 패키지 분리, DI 패턴 선택, 의존성 주입, Depends vs Container vs Dishka, Ruff 설정, mypy 설정, 린터 세팅, 포맷터 세팅, pre-commit 설정, import-linter, App Factory, create_app, lifespan, pydantic-settings, 환경변수 설정, config 구조, 레이어 분리, controllers/service/repository 구조, 도메인 레이어 도입 기준. NOT for: 단순 FastAPI 문법 질문 (Claude가 이미 앎), 일반적인 Ruff/mypy 사용법.
Install
npx skillscat add nomik94/claude-code-preset/fastapi Install via the SkillsCat registry.
SKILL.md
FastAPI 스킬
프로젝트 구조 (Folder-First)
controllers/, dto/, exceptions/, constants/는 처음부터 폴더로 생성. File→Package 진화 없음.
project-root/
├── pyproject.toml / poetry.lock / alembic.ini
├── .pre-commit-config.yaml / .importlinter
├── migrations/
│ ├── env.py
│ └── versions/
├── app/
│ ├── main.py # App factory (create_app)
│ ├── core/
│ │ ├── config/ # pydantic-settings (항상 폴더)
│ │ │ ├── __init__.py
│ │ │ ├── settings.py
│ │ │ ├── database.py
│ │ │ └── redis.py
│ │ ├── database.py # Engine, session, Base
│ │ ├── security/ # JWT, password, RBAC
│ │ │ ├── __init__.py
│ │ │ ├── jwt.py
│ │ │ ├── password.py
│ │ │ └── rbac.py
│ │ ├── exceptions/ # 애플리케이션 예외 (항상 폴더)
│ │ │ ├── __init__.py
│ │ │ ├── base.py # AppException hierarchy
│ │ │ ├── handlers.py # register_exception_handlers()
│ │ │ └── mappings.py # 도메인 예외 → HTTP 매핑 테이블
│ │ └── middleware/
│ │ ├── __init__.py
│ │ ├── logging.py
│ │ └── rate_limit.py
│ ├── common/
│ │ ├── base_repository.py # BaseRepository[ModelType] 제네릭
│ │ ├── base_dto.py # CamelModel base
│ │ ├── pagination.py
│ │ ├── types.py
│ │ └── decorators.py # @transactional, @retry, @log_execution
│ ├── {domain}/
│ │ ├── controllers/ # HTTP endpoints (항상 폴더)
│ │ │ ├── __init__.py
│ │ │ ├── admin_controller.py # admin 클라이언트용
│ │ │ ├── app_controller.py # 모바일 앱용
│ │ │ └── web_controller.py # 웹 클라이언트용
│ │ ├── dto/ # Pydantic DTOs (항상 폴더, 엔드포인트 1:1)
│ │ │ ├── __init__.py
│ │ │ ├── create_user.py # CreateUserRequest, CreateUserResponse
│ │ │ ├── update_user.py
│ │ │ ├── list_users.py
│ │ │ └── common.py # 공유 DTO (UserBase 등)
│ │ ├── constants/ # 도메인 상수 (항상 폴더)
│ │ │ ├── __init__.py
│ │ │ ├── enums.py
│ │ │ ├── messages.py
│ │ │ └── limits.py
│ │ ├── exceptions/ # 도메인 예외 (domain/ 밖)
│ │ │ ├── __init__.py
│ │ │ └── domain.py # 순수 도메인 예외
│ │ ├── dependencies.py # Depends() factories
│ │ ├── domain/ # Pure business logic (ZERO framework imports)
│ │ │ ├── entities.py
│ │ │ ├── value_objects.py
│ │ │ ├── services.py
│ │ │ ├── repositories.py # Protocol interfaces (Ports)
│ │ │ └── events.py
│ │ ├── application/
│ │ │ └── service.py
│ │ └── infrastructure/
│ │ ├── models.py # SQLAlchemy ORM
│ │ └── repository.py # Protocol impl
└── tests/
├── conftest.py
├── unit/{domain}/
└── integration/Folder-First 규칙
| 대상 | 규칙 |
|---|---|
| controllers/ | 처음부터 폴더. 클라이언트별 {role}_controller.py |
| dto/ | 처음부터 폴더. 엔드포인트별 파일 + common.py |
| exceptions/ | 처음부터 폴더. domain.py 하나로 시작 |
| constants/ | 처음부터 폴더. enums.py, messages.py, limits.py |
| domain/ 내부 | 파일로 시작 → 200줄+/클래스 3개+ 시 폴더 분리 |
| infrastructure/ | 파일로 시작 → 비대 시 폴더 분리 |
폴더 분리 시
__init__.pyre-export MUST. 외부 import 경로 변경 없어야 함.
레이어 책임
controllers/ → HTTP only. No business logic. EndpointPath 필수.
dto/ → Pydantic DTOs. from_domain() factory. 엔드포인트 1:1.
application/service.py → Use case orchestration. Tx boundary. Event publish.
domain/entities.py → Business rules enforced directly. ZERO framework imports.
domain/repositories.py → Protocol (Port). Knows nothing about DB.
infrastructure/repo.py → Protocol impl. Entity <-> Model conversion.
exceptions/domain.py → 순수 도메인 예외. HTTP 코드 없음.
constants/ → 도메인 상수. enums, messages, limits 분리.App Factory
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
def create_app() -> FastAPI:
settings = get_settings()
app = FastAPI(title=settings.app_name, lifespan=lifespan,
docs_url="/docs" if settings.debug else None)
register_exception_handlers(app) # mappings.py 기반
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(CORSMiddleware, ...)
return appDI 패턴 선택
| 규모 | 패턴 | 기준 |
|---|---|---|
| 소규모 (도메인 ≤3) | FastAPI Depends | 기본 DI |
| 중규모 (도메인 4-9) | Manual DI + Container | 수동 팩토리 클래스 |
| 대규모 (도메인 10+) | Dishka | IoC 컨테이너 |
FastAPI Depends (Default)
def get_user_repository(db: AsyncSession = Depends(get_db)) -> SqlAlchemyUserRepository:
return SqlAlchemyUserRepository(db)
def get_user_service(repo=Depends(get_user_repository), db=Depends(get_db)) -> UserApplicationService:
return UserApplicationService(repo=repo, db=db)Manual DI + Container (Medium)
class Container:
def __init__(self, db: AsyncSession) -> None:
self.user_repo = SqlAlchemyUserRepository(db)
self.user_service = UserApplicationService(repo=self.user_repo, db=db)
async def get_container(db: AsyncSession = Depends(get_db)) -> Container:
return Container(db)Dishka (Large)
class UserProvider(Provider):
user_repository = provide(SqlAlchemyUserRepository, provides=UserRepository, scope=Scope.REQUEST)
router = APIRouter(route_class=DishkaRoute)금지: dependency-injector (Cython 이슈, 단일 메인테이너)
도구 설정
Ruff
[tool.ruff]
target-version = "py313"
line-length = 120
[tool.ruff.lint]
select = ["E","W","F","I","N","UP","SIM","B","A","C4","RET","PIE","TCH","RUF",
"ASYNC","S","PT","T20","ARG","ERA","DTZ","G","ANN","TID"]
ignore = ["S101","B008","RUF012","ANN101","ANN102"]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101","ARG","S106","ANN"]
"migrations/**" = ["ERA","ANN"]mypy
[tool.mypy]
python_version = "3.13"
strict = true
plugins = ["pydantic.mypy", "sqlalchemy.ext.mypy.plugin"]
[[tool.mypy.overrides]]
module = ["cashews.*","apscheduler.*","celery.*"]
ignore_missing_imports = true
[[tool.mypy.overrides]]
module = "tests.*"
disallow_untyped_defs = false
[[tool.mypy.overrides]]
module = ["app.*.domain.*"] # Strictestimport-linter
[importlinter:contract:domain-clean]
name = Domain layer must be pure
type = forbidden
source_modules = app.users.domain, app.orders.domain
forbidden_modules = fastapi, sqlalchemy, pydantic, httpx, redis
[importlinter:contract:domain-independence]
name = Domains must not import each other
type = independence
modules = app.users.domain, app.orders.domain
[importlinter:contract:core-independence]
name = Core must not import domains
type = forbidden
source_modules = app.core
forbidden_modules = app.users, app.orderspre-commit
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
hooks: [{id: ruff, args: [--fix]}, {id: ruff-format}]
- repo: https://github.com/pre-commit/mirrors-mypy
hooks: [{id: mypy, additional_dependencies: [pydantic, sqlalchemy[mypy]]}]
- repo: local
hooks:
- {id: import-linter, entry: poetry run lint-imports, language: system}
- {id: conventional-commits, entry: scripts/check_commit_msg.sh, language: script, stages: [commit-msg]}추가 품질 도구
vulture app/ --min-confidence 80 # 사용하지 않는 코드 감지
deptry . # 사용하지 않는 의존성 감지
pip-audit # 알려진 취약점 감지체크리스트
- controllers/ 폴더로 생성 (router.py 아님)
- dto/ 폴더로 생성 (엔드포인트 1:1 매핑)
- exceptions/ 폴더로 생성 (domain.py 포함)
- constants/ 폴더로 생성 (enums.py, messages.py, limits.py)
- core/exceptions/mappings.py 존재
- EndpointPath 헬퍼 사용 (하드코딩 경로 금지)
- domain/에 framework import 없음
- import-linter 계약 통과
- Ruff target-version = "py313"
- Conventional Commits 사용