모던 파이썬 실전 #1 FastAPI 시작과 셋업

6 분 소요

모던 파이썬 기초중급고급 21편에서 다진 도구를 FastAPI 한 프로젝트로 종합합니다. 타입 힌트, dataclass/Pydantic, async/await, 의존성 주입, 테스트까지 — 지금까지 익힌 도구를 한곳에 모읍니다.

  • #1 시작과 셋업, OpenAPI ← 이번 글
  • #2 라우팅, Pydantic 모델, 의존성 주입
  • #3 DB 연동 — SQLAlchemy 2.x + Alembic
  • #4 인증 — OAuth2 패스워드 플로우 + JWT
  • #5 비동기와 백그라운드 작업
  • #6 테스트와 배포 — pytest, Docker, Railway/Fly

만드는 것은 작은 할 일 API — 사용자 회원가입/로그인, 할 일 CRUD, 비동기 작업 처리, 테스트, 배포까지 정리합니다. 매 글은 그 위에 한 단계씩 쌓는 구성입니다.

왜 FastAPI인가 #

Python 웹 프레임워크는 여럿입니다. Django, Flask, FastAPI, Litestar, Starlette 등. 각자 강점이 있습니다.

DjangoFlaskFastAPI
스타일풀스택 (배터리 포함)마이크로마이크로 + 타입
비동기부분 (4.0+)외부 라이브러리네이티브
타입 힌트 활용보조적보조적핵심 동작
자동 문서 (OpenAPI)별도 라이브러리별도빌트인
학습 곡선가파름매우 평탄평탄
데이터 검증DRF + 직접직접Pydantic 자동

FastAPI의 강점은 타입 힌트가 그대로 동작한다는 점입니다. 함수 시그니처를 적으면 그게 곧:

  • 입력 검증
  • 직렬화/역직렬화
  • OpenAPI 스키마 (Swagger UI 자동 생성)
  • 에디터 자동완성
  • 정적 타입 검사

이 다섯이 한 번에 일어납니다. 다른 프레임워크는 각각을 별도 설정으로 잡아야 합니다. 모던 파이썬 시리즈에서 다진 타입 힌트가 가장 잘 통하는 영역이 FastAPI이고, 그래서 트랙의 수렴점으로 잡았습니다.

프로젝트 셋업 #

기초 #1의 uv 흐름으로 시작합니다.

프로젝트 만들기
uv init todo-api --python 3.14
cd todo-api

의존성 설치 #

실행에 필요한 의존성
uv add "fastapi[standard]"

fastapi[standard] 한 줄에 따라오는 것들:

  • FastAPI — 프레임워크 본체
  • uvicorn — ASGI 서버 (개발/프로덕션)
  • pydantic — 데이터 검증
  • httpx — 테스트용 클라이언트 (다음 글들에서)
  • email-validatorEmailStr 검증
  • python-multipart — 폼/파일 업로드
  • fastapi-clifastapi CLI 명령

다음으로 개발용 도구.

개발 도구
uv add --dev pytest pytest-asyncio ruff pyright

고급 #7에서 본 도구상자에 더해, FastAPI 테스트의 표준은 pytest + httpx 조합입니다 (#6에서 자세히).

첫 엔드포인트 #

app/main.py
from fastapi import FastAPI

app = FastAPI(
    title="Todo API",
    version="0.1.0",
    description="모던 파이썬 실전 시리즈의 예제 API",
)

@app.get("/")
def read_root() -> dict[str, str]:
    return {"hello": "fastapi"}

이게 끝입니다. 7 줄의 파이썬으로 HTTP 서버가 됩니다.

실행 #

개발 모드
uv run fastapi dev app/main.py

fastapi dev가 하는 일:

  • uvicorn을 띄우고
  • 코드 변경 시 자동 리로드
  • 디버그 로그
  • LAN 노출 안 함 (127.0.0.1만)

브라우저에서 http://127.0.0.1:8000을 열면:

{"hello": "fastapi"}

더 중요한 — 자동 문서 #

같은 서버의 /docs로 가보세요. Swagger UI가 자동으로 떠 있습니다. 엔드포인트 목록, 입력 스키마, 응답 모델, 바로 호출해 볼 수 있는 “Try it out” 버튼까지 정리합니다. 코드 한 줄도 더 안 적었습니다.

/redoc으로 가면 ReDoc 스타일 문서도 같이 있습니다.

이게 FastAPI가 말하는 자동 문서의 정체입니다. 함수 시그니처 + 타입 힌트 + docstring이 그대로 OpenAPI 명세로 변환됩니다.

더 풍부한 엔드포인트 #

여러 메소드/경로
from fastapi import FastAPI

app = FastAPI(title="Todo API", version="0.1.0")

@app.get("/")
def read_root() -> dict[str, str]:
    """루트 — 헬스체크용."""
    return {"hello": "fastapi"}

@app.get("/items/{item_id}")
def read_item(item_id: int) -> dict[str, int]:
    """ID로 단일 아이템 조회."""
    return {"item_id": item_id}

@app.get("/items")
def list_items(skip: int = 0, limit: int = 10) -> dict[str, int]:
    """페이지네이션 쿼리 파라미터."""
    return {"skip": skip, "limit": limit}

여기서 일어나는 자동 변환:

  • 경로 매개변수 {item_id} → 함수 인자 item_id: int
    • int가 아니면 422 Unprocessable Entity 자동 응답
  • 쿼리 매개변수 skip, limit → 함수의 기본값 있는 인자
    • ?skip=10&limit=20 형태로 자동 파싱
  • 반환 타입 -> dict[str, int] → JSON 응답
  • docstring → Swagger UI의 설명

int 타입을 적었는데 자동으로 변환,검증이 되는 게 핵심입니다. Flask 라면:

🚫 Flask와 비교
@app.route("/items/<int:item_id>")
def read_item(item_id):
    return jsonify({"item_id": item_id})

<int:item_id>같이 라우터에 타입을 적고, 검증 실패 응답을 직접 처리해야 합니다. FastAPI는 함수 시그니처가 라우터 명세입니다.

Path, Query — 더 세밀한 검증 #

고급 검증
from fastapi import FastAPI, Path, Query
from typing import Annotated

app = FastAPI()

@app.get("/items/{item_id}")
def read_item(
    item_id: Annotated[int, Path(ge=1, le=10000, description="아이템 ID")],
    q: Annotated[str | None, Query(min_length=3, max_length=50)] = None,
):
    return {"item_id": item_id, "q": q}

Path(), Query()검증 규칙메타데이터를 추가합니다.

  • ge=1>= 1
  • le=10000<= 10000
  • min_length=3 — 최소 3자
  • description=... — Swagger 문서에 표시

Annotated[T, ...] 패턴이 고급 #6에서 본 그것입니다 — 타입에 메타데이터를 묶는 표준. FastAPI의 거의 모든 의존성 주입이 이 패턴 위에 만들어졌습니다.

응답 — 자동 직렬화 #

dataclass / Pydantic 모델 반환
from datetime import datetime, timezone
from pydantic import BaseModel

class Todo(BaseModel):
    id: int
    title: str
    done: bool
    created_at: datetime

@app.get("/todos/1")
def get_todo() -> Todo:
    return Todo(
        id=1,
        title="FastAPI 학습",
        done=False,
        created_at=datetime.now(timezone.utc),
    )

응답:

{
  "id": 1,
  "title": "FastAPI 학습",
  "done": false,
  "created_at": "2026-05-01T12:00:00+00:00"
}

datetime 같은 비-JSON 타입도 자동 변환 됩니다. Pydantic이 직렬화를 담당합니다. 이걸 직접 짜면 라우트마다 변환 코드를 써야 하지만, FastAPI는 반환 타입만 적으면 끝.

Todo 모델에 대한 자세한 내용은 #2에서.

에러 — HTTPException #

에러 응답
from fastapi import HTTPException

todos: dict[int, Todo] = {1: Todo(...)}

@app.get("/todos/{todo_id}")
def get_todo(todo_id: int) -> Todo:
    if todo_id not in todos:
        raise HTTPException(status_code=404, detail="Todo not found")
    return todos[todo_id]

raise HTTPException(...) 한 줄로 JSON 에러 응답이 나갑니다.

{"detail": "Todo not found"}

표준 HTTP 상태 코드 (404, 400, 500 등)와 함께. 다음 글들에서 사용자 정의 예외 핸들러도 다룹니다.

프로젝트 구조 — 처음부터 단정하게 #

작은 API 라도 처음부터 모듈/패키지로 나누면 기초 #7에서 본 흐름이 그대로 살아납니다.

기본 구조 (이번 시리즈가 채울 모양)
todo-api/
├── pyproject.toml
├── app/
│   ├── __init__.py
│   ├── main.py            # FastAPI 인스턴스, 라우터 등록
│   ├── core/
│   │   ├── config.py      # 환경 변수
│   │   └── security.py    # JWT, 패스워드 (#4)
│   ├── api/
│   │   ├── __init__.py
│   │   ├── deps.py        # 의존성 주입 (#2, #4)
│   │   ├── todos.py       # 할 일 라우터
│   │   └── auth.py        # 인증 라우터 (#4)
│   ├── models/            # SQLAlchemy 모델 (#3)
│   │   └── todo.py
│   ├── schemas/           # Pydantic 스키마 (#2)
│   │   └── todo.py
│   └── db/                # DB 연결 (#3)
│       └── session.py
├── alembic/               # 마이그레이션 (#3)
└── tests/                 # pytest (#6)
    └── test_todos.py

이번 글에서는 app/main.py 한 파일이지만, 다음 글부터 한 디렉터리씩 채워 나갑니다.

환경 변수 — pydantic-settings #

설정값(DB URL, JWT 시크릿 등)을 코드에 박지 말고 환경 변수로 분리하는 게 표준입니다. FastAPI 진영의 표준 도구는 pydantic-settings.

설치
uv add pydantic-settings
app/core/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    app_name: str = "Todo API"
    database_url: str = "sqlite+aiosqlite:///./todo.db"
    jwt_secret: str = "dev-only-change-me"
    jwt_expire_minutes: int = 60

    model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

settings = Settings()

.env 파일에서 자동으로 읽고, 환경 변수가 있으면 그걸로 덮어씁니다. 타입 검증까지 포함이라 잘못된 환경 변수는 시작 시점에 막힙니다.

.env
JWT_SECRET=production-secret-here
DATABASE_URL=postgresql+asyncpg://user:pw@localhost/todo

.env는 git에서 제외 (.gitignore에 추가). 보안 주의.

빠르게 정리한 첫 글의 결과물 #

app/main.py — 이번 글의 마무리
from fastapi import FastAPI, HTTPException
from app.core.config import settings

app = FastAPI(
    title=settings.app_name,
    version="0.1.0",
)

@app.get("/")
def read_root() -> dict[str, str]:
    return {"app": settings.app_name, "status": "ok"}

@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "healthy"}
실행
uv run fastapi dev app/main.py
# → http://127.0.0.1:8000
# → http://127.0.0.1:8000/docs

엔드포인트 두 개 + 자동 문서 + 환경 변수 분리. 다음 글의 출발점입니다.

정리 #

이번 글에서 잡은 것:

  • 왜 FastAPI — 타입 힌트가 검증/직렬화/문서/자동완성에 그대로 동작
  • uv add "fastapi[standard]" 한 줄에 uvicorn/pydantic/httpx 다 따라옴
  • fastapi dev로 자동 리로드 개발 모드
  • 함수 시그니처가 라우터 명세 — 경로/쿼리 매개변수 타입 변환 자동
  • /docs, /redoc — OpenAPI/Swagger UI 자동 생성
  • Annotated[T, Path(...)], Annotated[T, Query(...)]로 세밀한 검증
  • Pydantic 모델 반환 — datetime 등 자동 직렬화
  • HTTPException으로 에러 응답
  • pydantic-settings로 환경 변수 분리
  • 처음부터 모듈/패키지 구조

다음 글(#2 라우팅, Pydantic, 의존성 주입)에서는 APIRouter로 라우트를 분리하고, Pydantic 스키마를 본격적으로 사용하고, 의존성 주입으로 라우트 간 공통 로직(인증된 사용자, DB 세션 등)을 깔끔하게 풀어내는 패턴을 다룹니다.

X