FastAPI Concepts — Senior Backend Engineer's Cheat Sheet
Click any node for full explanation & code · Scroll/pinch to zoom · Drag to pan · Arrows show cause → effect relationships
45%
Architecture
Async-First Design
Arch
› concurrency under I/O wait — not raw speed
Blocking Call in Async
Gotcha
production gotcha
› requests.get() inside async kills the event loop
Lean Route Handlers
Arch
› orchestrators only — no business logic, no direct DB
Fat Handler Trap
Gotcha
scales badly
› fine at 3 endpoints, disaster at 30
Layer Order
Arch
› schemas → deps → routers → services
Service Layer
Arch
› testable without HTTP stack — pure Python
run_in_executor()
Fix
› push sync code to thread pool — unblocks event loop
Schemas — Pydantic V2
Three-Schema Pattern
Schema
› Create / Update / Response — never mix them
Schema Explosion
Tradeoff
40+ entities
› 120+ schema classes — cognitive overhead becomes real
Schema Inheritance
Mitigation
› PatientBase → PatientCreate / PatientResponse
from_attributes = True
Schema
› bridges ORM objects to Pydantic — without it: 500
exclude_unset PATCH
Schema
› only fields client sent — prevents null overwrite
field_validator
Schema
› single-field rules — @classmethod, raises ValueError
model_validator
Schema
› cross-field rules — start_date before end_date
PATCH null trap
Gotcha
production gotcha
› client sends null → column NOT NULL → DB write fails
setattr() PATCH Pattern
Fix
› safe update — never OrmModel(**dict) in PATCH
Routing
APIRouter + prefix
Routing
› never put routes directly on app
Static Before Dynamic
Gotcha
ordering bug
› /me before /{id} — registration order matters
Route Conflict Bug
Effect
silent failure
› "me" passed as patient_id → nonsense 404
Max Two Nesting Levels
Rule
› anchor at lowest owner that makes auth sense
URL Ownership Principle
Design
› /patients/{id}/appointments — DB ownership in URL
Transitive Ownership Trap
Antipattern
3-level nesting
› /patients/{id}/appts/{id}/notes — no global URL
HTTP Status Codes
201 / 204 Explicit
Status
› 201 POST created · 204 DELETE no content
400 vs 422
Distinction
› 422 = wrong shape · 400 = wrong meaning
401 vs 403
Distinction
› 401 = who are you · 403 = I know, you can't
Enumeration Attack Surface
Security
403 leaks existence
› return 404 instead of 403 to prevent resource enumeration
Manual 422 Antipattern
Antipattern
common mistake
› never raise 422 for business rule violations
503 Service Unavailable
Status
› DB/Redis unreachable — infrastructure, not your bug
Dependency Injection
get_db yield pattern
DI
› commit on success · rollback on exception
db.refresh() Stale Data Bug
Gotcha
production gotcha
› skip refresh → DetachedInstanceError in production
Dependency Deduplication
DI
› same get_db twice = one session — not two transactions
Chained Dependencies
DI
› get_db → get_current_user → require_admin chain
DI vs Flask globals
Comparison
› declarative at route level — beats g/current_app
dependency_overrides
Testing
› swap get_db in tests without touching route code
Auth in Dependencies
Pattern
› opt-in per route — no exclusion lists needed
Error Handling
Three Error Layers
Design
› route inline · service domain · global handler
Domain Exceptions
Pattern
› PatientNotFoundError — zero HTTP knowledge in service
HTTPException in Service
Antipattern
coupling bug
› service coupled to HTTP — breaks gRPC/CLI/WebSocket
Global Exception Handler
Pattern
› last resort — log traceback, return generic 500
return {"error": str(e)}
Antipattern
wrong twice
› exposes internals AND returns 200 status
Background Task Silent Drop
Gotcha
silent failure
› asyncio.create_task() exceptions silently dropped
Lifespan
Lifespan Context Manager
Pattern
› replaces @app.on_event — startup/shutdown with yield
Connection Leak
Gotcha
production gotcha
› worker restarts leave ghost connections → pool exhausted
Startup Order Matters
Rule
› init in dependency order — raise on failure, don't swallow
SIGTERM Grace Window
K8s
› 30s default — shutdown must complete within window
app.state resources
Pattern
› shared HTTP client, Redis — accessible anywhere
Middleware
Middleware Use Cases
MW
› logging · CORS · request ID · timing — never auth
Middleware Stack Order Bug
Gotcha
ordering trap
› last registered = outermost = runs first on request
Auth in Middleware Trap
Antipattern
exclusion list drift
› exclusion list drifts — new public route silently requires auth
call_next body stream
Detail
› response body is a stream — can't read without buffering
MW vs Dependency
Comparison
› MW=every req, runs on 404s · Dep=selected routes
Health Checks
Active Health Check
Health
› SELECT 1 · Redis ping — never just return 200
Liveness vs Readiness
K8s
› /health/live = process alive · /health/ready = deps ok
Conflation Trap
Gotcha
mass pod restart
› one check checks DB → DB hiccup → all pods restart
Health Response Shape
Design
› diagnostic info without exposing DB connection strings
Always-200 Health
Antipattern
lies to infra
› load balancer routes traffic to dead pod
pydantic-settings
Settings Class
Config
› BaseSettings — typed env vars, crash on startup if missing
Fail Fast at Startup
Pattern
› missing env var crashes import — not mid-request
Secret With Default
Security
credential leak
› silently falls back to dev credential in production
@lru_cache get_settings()
Pattern
› enables dependency_overrides in tests
Singleton Test Trap
Gotcha
test isolation issue
› module-level Settings() can't swap env vars between tests
JWT Authentication
JWT Auth Flow
Auth
› login → token → Bearer header → dep decodes → user injected
Signed Not Encrypted
Security
payload is public
› payload is base64-readable — never put PII in JWT
Refresh Token Pattern
Auth
› short-lived access + long-lived refresh — UX vs security
Stolen Refresh Token
Gotcha
30-day window
› without server-side revocation — valid for full expiry
IDOR Vulnerability
Security
auth bypass
› client provides patient_id param → any user reads any data
ID From Token Only
Fix
› patient_id always from verified JWT — never from query param
Same 401 for All Auth Failures
Security
› bad token + deleted user → both 401 — no info leak
SSE vs WebSockets
Use SSE When…
Design
› server→client only · HTTP · auto-reconnect · HTTP/2
Use WebSockets When…
Design
› bidirectional · chat · collaborative editing · gaming
SSE Proxy Buffering
Gotcha
nginx / ALB trap
› buffered responses — client sees nothing until close
X-Accel-Buffering: no
Fix
› nginx buffering off + ALB idle timeout config
StreamingResponse
API
› media_type text/event-stream — async generator yields chunks
Production Gotchas
Worker Global State
Gotcha
split-brain
› in-memory cache per worker — write in W1 invisible to W2
--reload in Production
Gotcha
never in prod
› file watchers consume CPU — unexpected restarts under load
Structured Logging
Pattern
› structlog / JSON formatter — queryable by log aggregators
print() in Production
Antipattern
unqueryable
› unstructured text — can't query in Datadog/CloudWatch
Connection Pool Sizing
Perf
› default pool_size=5 × 4 workers = 20 conns max
Pool Exhaustion
Gotcha
traffic spike
› pool at max under load = next connection blocks
Testing
TestClient vs AsyncClient
Testing
› sync TestClient first — AsyncClient only when needed
dependency_overrides Power
Testing
› full request-to-response test with in-memory DB
Session-Per-Test Pattern
Testing
› transaction wraps test, rollback after — clean state
No Mocking, No Patching
Testing
› DI system handles the swap — test real code paths