Overview
Use FastAPI for Python HTTP services that need OpenAPI, async I/O, and typed request and response models. It pairs well with postgres via SQLAlchemy or asyncpg, and ships with Pydantic v2. This page covers model design, dependency injection, async discipline, OpenAPI hygiene, and route layout.
Separate read and write Pydantic models
One model per direction. The model that accepts input is not the same as the one that returns output.
from pydantic import BaseModel, EmailStr
from datetime import datetime
class UserCreate(BaseModel):
email: EmailStr
password: str
class UserRead(BaseModel):
id: str
email: EmailStr
created_at: datetime
model_config = {"from_attributes": True}- Request models reject fields the client should not set (
id,created_at, role flags). - Response models hide fields the client should not see (
password_hash, internal status). - Use
model_config = {"from_attributes": True}to build a response model from an ORM instance. - Validate at the edge once. Do not re-validate the same payload three layers deep.
Inject dependencies with Depends
Use Depends for anything a route needs that is not in the request body or path: database sessions, the current user, settings, feature flags.
from fastapi import Depends, FastAPI
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
async def get_db() -> AsyncSession: ...
async def get_current_user(db: AsyncSession = Depends(get_db)) -> User: ...
@app.get("/me", response_model=UserRead)
async def me(user: User = Depends(get_current_user)) -> User:
return user- Dependencies are cached per request.
Depends(get_db)returns the same session everywhere in one request. - Use
yieldin a dependency to run teardown (commit, close) after the route returns. - Group dependencies into a single chain rather than passing them route by route.
Keep the entire request path async
One blocking call in an async route stalls the event loop for every other request.
- Use
asyncpg,httpx.AsyncClient, or async SQLAlchemy. Notpsycopg2,requests, or sync SQLAlchemy in async routes. - If a library has no async API, offload it with
await run_in_threadpool(...)orasyncio.to_thread(...). - Never call
time.sleep()in a route. Useawait asyncio.sleep(). - See python for asyncio rules outside FastAPI.
Set response_model, tags, and summary on every route
The OpenAPI schema is part of your API contract. Treat it like the contract.
@app.post(
"/users",
response_model=UserRead,
status_code=201,
tags=["users"],
summary="Create a user",
)
async def create_user(payload: UserCreate, db: AsyncSession = Depends(get_db)) -> User: ...response_modelfilters the route’s return value through Pydantic. Returning extra fields will not leak them.tagsgroup endpoints in the docs. One tag per logical resource.summaryis the short label shown in/docs.description(or the route docstring) is the long form.- Set
status_codeexplicitly. The default of200is wrong forPOSTandDELETE.
Use BackgroundTasks only for fire-and-forget work
BackgroundTasks runs after the response is sent. Use it for work the caller need not wait on and that is allowed to fail silently.
from fastapi import BackgroundTasks
@app.post("/signup")
async def signup(payload: UserCreate, tasks: BackgroundTasks) -> UserRead:
user = await create_user(payload)
tasks.add_task(send_welcome_email, user.email)
return userFor work that must survive a process restart (payments, webhooks, retries), use a real queue: Celery, RQ, Arq, or a database-backed job table. BackgroundTasks dies with the worker.
Run startup and shutdown in a lifespan context
The old @app.on_event decorators are deprecated. Use the async context manager.
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
app.state.db = create_engine(settings.database_url)
yield
await app.state.db.dispose()
app = FastAPI(lifespan=lifespan)Open pools, warm caches, and load models before yield. Close them after. Lifespan runs once per worker, not once per request.
Lay routes out under app/routers/
One file per resource. Each router defines router = APIRouter(prefix="/users", tags=["users"]) and is mounted in main.py.
app/
main.py # FastAPI() + lifespan + include_router calls
deps.py # Depends() factories
routers/
users.py
orders.py
models/ # Pydantic models
db/ # SQLAlchemy or asyncpg layer
Keep main.py short: imports, app construction, router mounts. Push the rest into modules.