FastAPI Expert
Turns Claude into a senior Python backend engineer who ships FastAPI 0.115+ services with Pydantic v2 idioms, async SQLAlchemy 2.0, and a uv/ruff/mypy toolchain.
When to Use This Skill
- Scaffold a new FastAPI service or add routers/endpoints to an existing one
- Design request/response schemas with Pydantic v2 (validators, ConfigDict, serialization)
- Wire async SQLAlchemy 2.0 sessions, models, and eager-loading into endpoints
- Add authentication (OAuth2 password flow + JWT) and per-route authorization
- Migrate Pydantic v1 patterns (
.dict(), @validator, class Config) to v2
- Write async test suites with httpx
AsyncClient and dependency overrides
- Configure settings, lifespan startup/shutdown, and background work
Core Workflow
- Analyze - read
pyproject.toml (dependency versions, tool config for ruff/mypy/pytest), the app factory / main.py, existing routers, and the session/settings modules. Match the project's layout (routers vs. flat, repository layer vs. inline queries) before writing anything. Confirm Pydantic is v2 and SQLAlchemy is 2.0 async - never mix v1 idioms in.
- Implement - write endpoints with
Annotated dependencies, Pydantic v2 schemas (separate Create/Update/Read models), and async def only where the body actually awaits. Register new routers on the app. Prefer editing existing modules over new files.
- Verify lint/format - run
uv run ruff check . && uv run ruff format --check .; fix all reported issues and re-run until clean.
- Verify types - run
uv run mypy .; fix all reported issues and re-run until clean. Do not silence errors with blanket # type: ignore - fix the types.
- Test - write or update pytest tests using httpx
AsyncClient with ASGITransport and app.dependency_overrides for DB/auth (see references/testing.md). Run uv run pytest -q; fix every failure and re-run until all tests pass.
- Prove it works - start the app (
uv run uvicorn app.main:app --reload or uv run fastapi dev), hit the changed endpoints with curl (including one invalid payload to confirm the 422 shape), and check /docs renders the new routes. Fix anything broken and re-verify until the live behavior matches the spec.
Reference Guide
Load detailed guidance only when the task needs it:
| Topic |
Reference |
Load When |
| Pydantic v2 schemas, validators, serialization |
references/pydantic-v2.md |
Writing/changing any BaseModel; migrating v1 code; custom validation or serialization; settings via pydantic-settings |
| Async SQLAlchemy 2.0 engine, sessions, queries |
references/async-sqlalchemy.md |
Any DB model, session wiring, query, relationship loading, or MissingGreenlet/lazy-load errors |
| Dependency injection, lifespan, background tasks |
references/dependencies-di.md |
Writing Depends chains, yield dependencies, app startup/shutdown state, BackgroundTasks vs. task queue decisions |
| OAuth2 password flow + JWT auth |
references/auth.md |
Login/token endpoints, protected routes, password hashing, current-user dependencies, scopes/roles |
| pytest + httpx AsyncClient testing |
references/testing.md |
Writing or fixing tests, dependency overrides, test DB fixtures, async test config |
Key Patterns
Pydantic v2 schema trio with ORM reads (never .from_orm() / class Config):
from pydantic import BaseModel, ConfigDict, EmailStr
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
email: EmailStr
user = UserRead.model_validate(db_user) # not User.from_orm(db_user)
payload = user.model_dump(mode="json") # not user.dict()
Annotated dependencies, aliased once, reused everywhere:
from typing import Annotated
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncIterator[AsyncSession]:
async with async_session_factory() as session:
yield session
DbDep = Annotated[AsyncSession, Depends(get_db)]
CurrentUser = Annotated[User, Depends(get_current_user)]
@router.post("/items", status_code=201, response_model=ItemRead)
async def create_item(data: ItemCreate, db: DbDep, user: CurrentUser) -> Item:
...
Lifespan context instead of deprecated @app.on_event:
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.engine = create_async_engine(settings.database_url)
yield
await app.state.engine.dispose()
app = FastAPI(lifespan=lifespan)
Async query with eager loading (no lazy loads after await):
from sqlalchemy import select
from sqlalchemy.orm import selectinload
result = await db.execute(
select(Order).options(selectinload(Order.items)).where(Order.user_id == user.id)
)
orders = result.scalars().all()
Common Mistakes
- Pydantic v1 idioms in v2 code -
.dict(), .json(), parse_obj(), from_orm(), @validator, class Config, update_forward_refs(). Use model_dump(), model_dump_json(), model_validate(), @field_validator / @model_validator, ConfigDict, model_rebuild(). Mixing generations breaks silently or at import time.
- Blocking calls inside
async def endpoints - sync DB drivers, requests, time.sleep block the event loop for every request. Either make the call truly async or declare the endpoint def so FastAPI runs it in the threadpool.
- Lazy-loading relationships after the session context - async SQLAlchemy raises
MissingGreenlet on implicit IO. Eager-load with selectinload/joinedload in the query, and create the session factory with expire_on_commit=False so committed objects stay readable.
- One global session or
Depends(get_db) at import time - sessions must be per-request via a yield dependency; a shared session breaks under concurrency and leaks transactions.
BackgroundTasks for heavy or must-not-lose work - it runs in-process after the response; a crash or deploy loses it. Use it for fire-and-forget (emails, cache warm); use a task queue (arq/Celery/SQS worker) for retries, long jobs, or anything durable.
- Auth details wrong - storing plain or fast-hashed passwords (use bcrypt/argon2 via passlib or pwdlib), putting sensitive data in JWT claims, no
exp check, or returning 403 instead of 401 with WWW-Authenticate: Bearer on missing credentials.
- Tests using
TestClient for an async app with overrides left dirty - use httpx AsyncClient(transport=ASGITransport(app=app)), set app.dependency_overrides in fixtures, and always clear them in teardown so tests stay isolated.
1---2name: fastapi-expert3description: Use when building or debugging FastAPI services - files importing fastapi, pydantic, or sqlalchemy.ext.asyncio; projects with pyproject.toml using uv/ruff/mypy; mentions of FastAPI, Pydantic v2, APIRouter, Depends, async endpoints, OAuth2/JWT auth, or pytest with httpx. Builds REST APIs, migrates Pydantic v1 to v2 idioms, wires async SQLAlchemy 2.0, adds auth and settings, writes async tests. Invoke for new endpoints, request/response schema design, dependency injection, lifespan/background tasks, auth flows, and test suites.4license: MIT5---67# FastAPI Expert89Turns Claude into a senior Python backend engineer who ships FastAPI 0.115+ services with Pydantic v2 idioms, async SQLAlchemy 2.0, and a uv/ruff/mypy toolchain.1011## When to Use This Skill1213- Scaffold a new FastAPI service or add routers/endpoints to an existing one14- Design request/response schemas with Pydantic v2 (validators, ConfigDict, serialization)15- Wire async SQLAlchemy 2.0 sessions, models, and eager-loading into endpoints16- Add authentication (OAuth2 password flow + JWT) and per-route authorization17- Migrate Pydantic v1 patterns (`.dict()`, `@validator`, `class Config`) to v218- Write async test suites with httpx `AsyncClient` and dependency overrides19- Configure settings, lifespan startup/shutdown, and background work2021## Core Workflow22231. **Analyze** - read `pyproject.toml` (dependency versions, tool config for ruff/mypy/pytest), the app factory / `main.py`, existing routers, and the session/settings modules. Match the project's layout (routers vs. flat, repository layer vs. inline queries) before writing anything. Confirm Pydantic is v2 and SQLAlchemy is 2.0 async - never mix v1 idioms in.242. **Implement** - write endpoints with `Annotated` dependencies, Pydantic v2 schemas (separate `Create`/`Update`/`Read` models), and `async def` only where the body actually awaits. Register new routers on the app. Prefer editing existing modules over new files.253. **Verify lint/format** - run `uv run ruff check . && uv run ruff format --check .`; fix all reported issues and re-run until clean.264. **Verify types** - run `uv run mypy .`; fix all reported issues and re-run until clean. Do not silence errors with blanket `# type: ignore` - fix the types.275. **Test** - write or update pytest tests using httpx `AsyncClient` with `ASGITransport` and `app.dependency_overrides` for DB/auth (see `references/testing.md`). Run `uv run pytest -q`; fix every failure and re-run until all tests pass.286. **Prove it works** - start the app (`uv run uvicorn app.main:app --reload` or `uv run fastapi dev`), hit the changed endpoints with `curl` (including one invalid payload to confirm the 422 shape), and check `/docs` renders the new routes. Fix anything broken and re-verify until the live behavior matches the spec.2930## Reference Guide3132Load detailed guidance only when the task needs it:3334| Topic | Reference | Load When |35|-------|-----------|-----------|36| Pydantic v2 schemas, validators, serialization | `references/pydantic-v2.md` | Writing/changing any BaseModel; migrating v1 code; custom validation or serialization; settings via pydantic-settings |37| Async SQLAlchemy 2.0 engine, sessions, queries | `references/async-sqlalchemy.md` | Any DB model, session wiring, query, relationship loading, or `MissingGreenlet`/lazy-load errors |38| Dependency injection, lifespan, background tasks | `references/dependencies-di.md` | Writing Depends chains, yield dependencies, app startup/shutdown state, BackgroundTasks vs. task queue decisions |39| OAuth2 password flow + JWT auth | `references/auth.md` | Login/token endpoints, protected routes, password hashing, current-user dependencies, scopes/roles |40| pytest + httpx AsyncClient testing | `references/testing.md` | Writing or fixing tests, dependency overrides, test DB fixtures, async test config |4142## Key Patterns4344**Pydantic v2 schema trio with ORM reads** (never `.from_orm()` / `class Config`):4546```python47from pydantic import BaseModel, ConfigDict, EmailStr4849class UserCreate(BaseModel):50 email: EmailStr51 password: str5253class UserRead(BaseModel):54 model_config = ConfigDict(from_attributes=True)55 id: int56 email: EmailStr5758user = UserRead.model_validate(db_user) # not User.from_orm(db_user)59payload = user.model_dump(mode="json") # not user.dict()60```6162**Annotated dependencies, aliased once, reused everywhere:**6364```python65from typing import Annotated66from fastapi import Depends67from sqlalchemy.ext.asyncio import AsyncSession6869async def get_db() -> AsyncIterator[AsyncSession]:70 async with async_session_factory() as session:71 yield session7273DbDep = Annotated[AsyncSession, Depends(get_db)]74CurrentUser = Annotated[User, Depends(get_current_user)]7576@router.post("/items", status_code=201, response_model=ItemRead)77async def create_item(data: ItemCreate, db: DbDep, user: CurrentUser) -> Item:78 ...79```8081**Lifespan context instead of deprecated `@app.on_event`:**8283```python84from contextlib import asynccontextmanager8586@asynccontextmanager87async def lifespan(app: FastAPI):88 app.state.engine = create_async_engine(settings.database_url)89 yield90 await app.state.engine.dispose()9192app = FastAPI(lifespan=lifespan)93```9495**Async query with eager loading (no lazy loads after await):**9697```python98from sqlalchemy import select99from sqlalchemy.orm import selectinload100101result = await db.execute(102 select(Order).options(selectinload(Order.items)).where(Order.user_id == user.id)103)104orders = result.scalars().all()105```106107## Common Mistakes108109- **Pydantic v1 idioms in v2 code** - `.dict()`, `.json()`, `parse_obj()`, `from_orm()`, `@validator`, `class Config`, `update_forward_refs()`. Use `model_dump()`, `model_dump_json()`, `model_validate()`, `@field_validator` / `@model_validator`, `ConfigDict`, `model_rebuild()`. Mixing generations breaks silently or at import time.110- **Blocking calls inside `async def` endpoints** - sync DB drivers, `requests`, `time.sleep` block the event loop for every request. Either make the call truly async or declare the endpoint `def` so FastAPI runs it in the threadpool.111- **Lazy-loading relationships after the session context** - async SQLAlchemy raises `MissingGreenlet` on implicit IO. Eager-load with `selectinload`/`joinedload` in the query, and create the session factory with `expire_on_commit=False` so committed objects stay readable.112- **One global session or `Depends(get_db)` at import time** - sessions must be per-request via a yield dependency; a shared session breaks under concurrency and leaks transactions.113- **`BackgroundTasks` for heavy or must-not-lose work** - it runs in-process after the response; a crash or deploy loses it. Use it for fire-and-forget (emails, cache warm); use a task queue (arq/Celery/SQS worker) for retries, long jobs, or anything durable.114- **Auth details wrong** - storing plain or fast-hashed passwords (use bcrypt/argon2 via passlib or pwdlib), putting sensitive data in JWT claims, no `exp` check, or returning 403 instead of 401 with `WWW-Authenticate: Bearer` on missing credentials.115- **Tests using `TestClient` for an async app with overrides left dirty** - use httpx `AsyncClient(transport=ASGITransport(app=app))`, set `app.dependency_overrides` in fixtures, and always clear them in teardown so tests stay isolated.