zapEngineering
FastAPI
Async-first APIs: dependency injection, Pydantic validation, async correctness, and production serving.
1 item
Links1
When to reach for FastAPI
Async-first APIs, lots of I/O fan-out (calling other services, DBs, LLM APIs), and anything where you want automatic OpenAPI docs and Pydantic validation at the edge. It's my default for new standalone services and for AI-facing backends.
Dependency injection is the core idea
Depends() is how you wire in DB sessions, the current user, settings, rate limiters, etc. Dependencies are resolved per-request and can be cached, nested, and overridden in tests.
async def get_db() -> AsyncSession:
async with SessionLocal() as session:
yield session
@app.get("/clients/{id}")
async def read_client(id: int, db: AsyncSession = Depends(get_db)):
return await client_service.get(db, id)
Override dependencies in tests with app.dependency_overrides — no monkeypatching needed.
Pydantic v2 at the boundary
- Define explicit request and response models. Use
response_model=so you never leak internal fields (password hashes, soft-delete flags) by accident. - Pydantic v2 is significantly faster (Rust core). Use
model_configfor things likefrom_attributes=Trueto read straight from ORM objects.
Async pitfalls specific to FastAPI
- A sync
defendpoint runs in a threadpool (fine, but limited threads). An asyncdefendpoint runs on the event loop — never block it. - Use an async DB driver (
asyncpgvia SQLAlchemy async, or async Tortoise) in async endpoints. A sync driver insideasync defsilently serializes all requests. BackgroundTasksis fine for fire-and-forget work that's cheap and non-critical (sending an email). For anything that must complete or must survive a crash, use a real queue or Temporal — not background tasks.
Production serving
uvicornworkers behindgunicorn(oruvicorn --workers), fronted by Nginx. Set worker count to roughly2 × coresfor I/O-bound loads, then tune by measurement.- Add
/health(liveness) and/ready(readiness — checks DB/Redis) endpoints for orchestrators.