FastAPI (Python)
Pathrule4 Rules • 2 Memories • 1 Skill
An opinionated baseline for shipping FastAPI services on Python 3.12+. It keeps request and response bodies behind Pydantic v2 models, runs I/O routes as async without blocking the event loop, wires auth, database sessions, and configuration through dependency injection instead of globals, and manages schema changes with forward-only Alembic migrations. These are the conventions that keep a FastAPI app fast, typed, and testable as it grows past the tutorial.
Suggested path map
Pathrule places each piece on the matching path, so your assistant only sees it where it belongs. This is the scoping you get on import; you can adjust it in your workspace.
Rules
4Type every request and response with a Pydantic model/app/schemashighstrictDeclare Pydantic models for request bodies and use response_model on every route; never accept or return a raw dict.
| 1 | FastAPI's validation, serialization, and OpenAPI docs all flow from type hints. An untyped `dict` body opts out of all three and ships unvalidated input straight into your code. |
| 2 | |
| 3 | - Type every request body, query, and path parameter. Use a Pydantic `BaseModel` for bodies; never declare a parameter as a bare `dict` or `Any`. |
| 4 | - Set `response_model=` (or a typed return annotation) on each route so output is validated and filtered. This is also what stops an ORM object from leaking password hashes or internal fields into the response. |
| 5 | - Keep separate models for input and output (e.g. `UserCreate` vs `UserRead`); do not reuse one model that both accepts a password and returns the row. |
| 6 | - Use Pydantic v2 idioms: `model_config = ConfigDict(from_attributes=True)` to read from ORM objects, `Field(...)` for constraints and examples. Validate at the boundary so the rest of the handler works with trusted, typed data. |
Do not block the event loop in async routes/app/routershighstrictInside async def, only await non-blocking I/O; run blocking/CPU work in a threadpool or use a def route so FastAPI offloads it.
| 1 | An `async def` route runs on the event loop. One blocking call inside it freezes every concurrent request on that worker, turning a fast server into a slow one under load. |
| 2 | |
| 3 | - In an `async def` route, only `await` async-native I/O (async DB driver, `httpx.AsyncClient`, async cache client). Never call a synchronous, blocking client (`requests`, a sync DB cursor, `time.sleep`) directly inside it. |
| 4 | - If a dependency is only available as blocking code, either define the route as a plain `def` (FastAPI runs it in a threadpool automatically) or offload the blocking call with `anyio.to_thread.run_sync` / `run_in_executor`. |
| 5 | - Do CPU-bound work (image processing, heavy parsing) off the event loop - a threadpool for the GIL-friendly cases, a process pool or a background worker/queue for the rest. Do not grind CPU inside an async route. |
| 6 | - Be consistent: an async route calling a sync function calling async code is where event-loop bugs hide. Pick async or sync per route and keep the chain coherent. |
Inject dependencies with Depends; no module globals for request state/apphighadvisoryProvide DB sessions, the current user, and settings through Depends; never reach for a module-level global mutated per request.
| 1 | FastAPI's dependency injection is how request-scoped state stays correct and testable. A module global shared across requests is a race condition and an untestable seam. |
| 2 | |
| 3 | - Provide the database session, the authenticated user, pagination, and config as dependencies (`db: Session = Depends(get_db)`, `user: User = Depends(get_current_user)`). Do not store the session or current user in a module-level variable. |
| 4 | - Make the session dependency yield-based so setup and teardown (commit/rollback/close) are guaranteed per request: `def get_db(): db = SessionLocal(); try: yield db; finally: db.close()`. |
| 5 | - Compose dependencies for cross-cutting concerns (auth, role checks, rate limits) and attach them at the router level with `dependencies=[Depends(...)]` when every route needs them. |
| 6 | - Override dependencies in tests with `app.dependency_overrides` to inject a test DB or a fake user. This only works if state actually flows through `Depends`, which is the point. |
Change the schema only through forward-only Alembic migrations/apphighadvisoryEvery schema change ships as a reviewed Alembic migration; never autocreate tables in production or edit the DB by hand.
| 1 | The database schema is shared state across every deploy and environment. An untracked change is a deploy that works on one machine and breaks on the next. |
| 2 | |
| 3 | - Generate a migration for every model change with `alembic revision --autogenerate`, then read and edit it. Autogenerate misses some changes (type tweaks, server defaults, constraints, enums); never ship the generated file unread. |
| 4 | - Do not rely on `Base.metadata.create_all()` in production. It is fine for a test fixture; it does not evolve an existing schema and will silently drift from your migrations. |
| 5 | - Write migrations forward-only and deploy-safe: add columns nullable or with a default, backfill, then enforce constraints in a later migration. Avoid a single migration that locks a large table for the whole deploy. |
| 6 | - Commit migrations with the code change that needs them, run them as a gated step before the new code serves traffic, and keep one linear history (resolve multiple heads before merging). |
Memories
2Project layout and app wiring/appCanonical FastAPI structure - routers, schemas, services, deps - and a lifespan-based app bootstrap.
| 1 | We keep a FastAPI service organized by responsibility so routes stay thin and logic stays testable. |
| 2 | |
| 3 | - Layout: `app/main.py` (create the app, include routers), `app/routers/` (one `APIRouter` per resource), `app/schemas/` (Pydantic models), `app/models/` (ORM models), `app/services/` (business logic), `app/deps.py` (shared dependencies), `app/core/` (settings, security). |
| 4 | - Routes stay thin: validate via the schema, delegate to a service function, return a typed response. Business logic and DB queries live in services, not in the route handler. |
| 5 | - Mount feature routers with `app.include_router(router, prefix='/users', tags=['users'])`; group related endpoints under one `APIRouter` with shared dependencies. |
| 6 | - Use the `lifespan` async context manager for startup/shutdown (DB pool, clients) rather than the deprecated `@app.on_event` hooks. Acquire resources on enter, release on exit. |
| 7 | - Run with `uvicorn` (one worker per process; scale with multiple workers behind a process manager or `gunicorn -k uvicorn.workers.UvicornWorker`). |
| 8 | |
| 9 | See /app for the settings memory and the Pydantic, async, and DI rules. |
Configuration via pydantic-settings/app/coreLoad all config from environment through a typed pydantic-settings model; never read os.environ scattered across the code.
| 1 | Configuration is typed and centralized, not `os.environ.get` calls sprinkled through the codebase. |
| 2 | |
| 3 | - Define a `Settings(BaseSettings)` model with `pydantic-settings`, typing each field (`database_url: str`, `jwt_secret: str`, `debug: bool = False`). It loads from environment variables and a `.env` file and validates types at startup. |
| 4 | - Provide settings as a cached dependency (`@lru_cache` on a `get_settings()` factory, exposed via `Depends`) so the app reads and validates config once and tests can override it. |
| 5 | - Keep secrets out of the repo: `.env` is git-ignored and injected at runtime per environment. A missing required setting should fail fast at startup, not at the first request that needs it. |
| 6 | - Read config only through the settings object, never `os.environ` directly in handlers or services, so every configurable value is discoverable in one typed place. |
| 7 | |
| 8 | See /app for the project layout memory; see the secrets-env-management pattern for rotation and injection. |
Skills
1fastapi-endpoint-checklist/rootChecklist for adding or changing a FastAPI endpoint: schemas, async, dependencies, status codes, and migrations.
| 1 | --- |
| 2 | name: fastapi-endpoint-checklist |
| 3 | description: Checklist for adding or changing a FastAPI endpoint. Run before merging any router, schema, dependency, or model change. |
| 4 | --- |
| 5 | |
| 6 | # FastAPI endpoint checklist |
| 7 | |
| 8 | - [ ] Request body/query/path params are typed; bodies use a Pydantic model - no bare `dict`/`Any`. |
| 9 | - [ ] `response_model` (or typed return) is set; input and output models are separate so internal fields don't leak. |
| 10 | - [ ] Route is `async def` only if it awaits non-blocking I/O; any blocking/CPU work runs in a threadpool, a `def` route, or a worker. |
| 11 | - [ ] DB session and current user come through `Depends`; the session dependency is yield-based with commit/rollback/close. |
| 12 | - [ ] Auth/role checks attached as router or route dependencies; handler authorizes the specific action. |
| 13 | - [ ] Correct status codes (`201` on create, `204` on delete) and `HTTPException` for expected failures with a consistent error shape. |
| 14 | - [ ] Logic lives in a service function; the route stays thin (validate → delegate → return). |
| 15 | - [ ] Schema changes ship as a reviewed Alembic migration (autogenerate then read/edit); no `create_all` in prod; one linear migration history. |
| 16 | - [ ] Config is read from the pydantic-settings object, not `os.environ` inline. |
| 17 | - [ ] Tests override dependencies via `app.dependency_overrides` (test DB / fake user). |
Why this pattern
AI agents writing FastAPI accept untyped dict bodies, run blocking calls inside async routes, reach for module globals instead of dependencies, and edit the schema without a migration.
Built for Python teams building and maintaining FastAPI services.
Keeps your assistant from:
- Accepting untyped dict request bodies with no Pydantic validation
- Calling blocking sync DB or HTTP clients inside an async def route
- Using module-level globals for the DB session or current user instead of Depends
- Changing the database schema without an Alembic migration
- License
- Apache-2.0
- Version
- 1.0.0
- Updated
- 2026-06-09