⚡ FastAPI Boot Camp — Part 3: Async, Middleware, Routers & Production Patterns
The architecture knowledge that separates junior from senior
Goal: Understand async correctly, structure real applications with routers, and know the production patterns that FDE interviewers specifically probe.
1. Async — When to Use It and When Not To
This is the most misunderstood part of FastAPI. The rule is simple but important:
| Operation | Use | Why |
|---|---|---|
| DB query (async driver) | async def |
Non-blocking I/O |
| External HTTP request | async def |
Waiting on network |
| CPU-heavy computation | def (sync) |
Async doesn't help here |
| File read (small) | Either | Negligible difference |
| Calling a sync library | def |
Can't await sync code |
import httpx # async-compatible HTTP client
# ✅ CORRECT — I/O bound: use async
@app.get("/weather/{city}")
async def get_weather(city: str):
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.weather.com/{city}")
return response.json()
# ✅ CORRECT — CPU bound or sync library: use regular def
@app.get("/compute/{n}")
def heavy_compute(n: int):
result = sum(i ** 2 for i in range(n)) # CPU work, async won't help
return {"result": result}
# ❌ WRONG — calling blocking code inside async def freezes the entire server
@app.get("/bad/")
async def bad_endpoint():
import time
time.sleep(5) # THIS BLOCKS THE EVENT LOOP — kills concurrent performance
return {"status": "done"}
# ✅ FIX — run blocking work in a thread pool
import asyncio
from fastapi.concurrency import run_in_threadpool
@app.get("/ok/")
async def good_endpoint():
result = await run_in_threadpool(blocking_function, arg1, arg2)
return {"result": result}
Interview line: "
async defin FastAPI doesn't make code faster on its own — it only helps when you're waiting on I/O. Blocking calls inside an async route freeze the event loop and kill concurrency."
2. Application Routers — Structuring Real Projects
Don't put everything in main.py. Use APIRouter to split routes into modules.
Project structure
project/
├── main.py
├── routers/
│ ├── items.py
│ ├── users.py
│ └── orders.py
├── models/
│ └── schemas.py
└── dependencies.py
routers/items.py
from fastapi import APIRouter, Depends, HTTPException
from dependencies import get_current_user
router = APIRouter(
prefix="/items", # All routes start with /items
tags=["items"], # Groups routes in /docs
dependencies=[Depends(get_current_user)], # Applied to ALL routes in this router
responses={404: {"description": "Not found"}},
)
@router.get("/") # Full path: GET /items/
def list_items():
return [{"name": "Widget"}, {"name": "Gadget"}]
@router.get("/{item_id}") # Full path: GET /items/{item_id}
def get_item(item_id: int):
return {"item_id": item_id}
@router.post("/") # Full path: POST /items/
def create_item():
return {"created": True}
main.py
from fastapi import FastAPI
from routers import items, users, orders
app = FastAPI()
app.include_router(items.router)
app.include_router(users.router)
app.include_router(orders.router, prefix="/v2") # Override/extend prefix
3. Middleware
Middleware runs on every request/response. Use for logging, timing, CORS, auth headers.
Custom middleware
import time
from fastapi import Request
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start = time.time()
response = await call_next(request) # call the actual route handler
process_time = time.time() - start
response.headers["X-Process-Time"] = str(process_time)
return response
CORS Middleware — almost always needed
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com", "http://localhost:3000"], # or ["*"] for dev
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Interview tip: If asked "how do you handle CORS?", say: "I add
CORSMiddlewarewith explicit origin allowlists in production — never*in production if credentials are involved."
4. Lifespan Events — Startup & Shutdown
For initializing DB pools, loading ML models, connecting to message queues.
from contextlib import asynccontextmanager
# Modern approach (05-FastAPI 0.93+)
@asynccontextmanager
async def lifespan(app: FastAPI):
# STARTUP: runs before the app starts serving requests
print("Starting up...")
await db_pool.connect()
load_ml_model()
yield # ← app runs here
# SHUTDOWN: runs when the app is stopping
print("Shutting down...")
await db_pool.disconnect()
app = FastAPI(lifespan=lifespan)
5. Response Types Beyond JSON
from fastapi.responses import (
JSONResponse,
HTMLResponse,
PlainTextResponse,
FileResponse,
StreamingResponse,
RedirectResponse,
)
# HTML response
@app.get("/page", response_class=HTMLResponse)
def get_page():
return "<html><body><h1>Hello</h1></body></html>"
# File download
@app.get("/download/{filename}")
def download_file(filename: str):
return FileResponse(
path=f"/files/{filename}",
media_type="application/octet-stream",
filename=filename # sets Content-Disposition header
)
# Streaming — for large files or real-time data
import io
def generate_csv():
yield "id,name,price\n"
for item in get_all_items(): # doesn't load all into memory
yield f"{item.id},{item.name},{item.price}\n"
@app.get("/export/csv")
def export_csv():
return StreamingResponse(
generate_csv(),
media_type="text/csv",
headers={"Content-Disposition": "attachment; filename=export.csv"}
)
# Redirect
@app.get("/old-path")
def legacy_redirect():
return RedirectResponse(url="/new-path", status_code=301)
6. Security Patterns
API Key auth
from fastapi.security import APIKeyHeader
from fastapi import Security
API_KEY = "secret-key-from-env"
api_key_header = APIKeyHeader(name="X-API-Key")
def verify_api_key(api_key: str = Security(api_key_header)):
if api_key != API_KEY:
raise HTTPException(status_code=403, detail="Invalid API key")
return api_key
@app.get("/protected/", dependencies=[Depends(verify_api_key)])
def protected_route():
return {"data": "sensitive"}
JWT Bearer token (common pattern)
from jose import jwt, JWTError # pip install 04-python-jose
from fastapi.security import OAuth2PasswordBearer
SECRET_KEY = "your-secret"
ALGORITHM = "HS256"
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token")
def get_current_user(token: str = Depends(oauth2_scheme)):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub")
if user_id is None:
raise credentials_exception
except JWTError:
raise credentials_exception
user = get_user(user_id)
if not user:
raise credentials_exception
return user
7. Testing FastAPI
from fastapi.testclient import TestClient
from main import app
client = TestClient(app)
def test_get_item():
response = client.get("/items/1")
assert response.status_code == 200
assert response.json() == {"item_id": 1}
def test_item_not_found():
response = client.get("/items/9999")
assert response.status_code == 404
def test_create_item_validation_error():
response = client.post("/items/", json={"price": -1}) # missing name, invalid price
assert response.status_code == 422
def test_create_item():
response = client.post("/items/", json={"name": "Widget", "price": 9.99})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Widget"
8. The Interview Scenario Table — FastAPI Edition
| Question | Weak answer | Strong answer |
|---|---|---|
| "How does FastAPI handle validation?" | "It uses Pydantic" | "Pydantic models are bound to route parameters via type hints — FastAPI parses the request, runs Pydantic validation, and returns a 422 with structured error details automatically if it fails. No manual validation code needed." |
"When would you use async def?" |
"For performance" | "Only for I/O-bound operations — DB queries, external HTTP calls. CPU-bound work or sync libraries should stay in regular def. Blocking calls inside async def freeze the event loop." |
| "How do you share logic across routes?" | "I copy it" | "Dependency injection with Depends(). I write the logic once — auth, DB sessions, pagination — and compose it into routes. FastAPI resolves the dependency graph, including nested dependencies." |
| "How do you structure a large FastAPI app?" | "One big main.py" | "APIRouter per domain (items, users, orders), each in its own module. main.py just calls include_router. Shared auth and DB deps go in a dependencies.py." |
| "How do you avoid leaking sensitive fields?" | "I just don't include them" | "response_model on the route — define a separate output schema that excludes sensitive fields. FastAPI filters the output against that schema before serializing." |
✅ Part 3 Checklist
- [ ] Can explain when
async defhelps and when it doesn't (I/O vs CPU) - [ ] Know that blocking inside
async defis a bug, not a feature - [ ] Can structure a project with
APIRouter+include_router - [ ] Know how CORS middleware works and why
*is unsafe in production - [ ] Know the
lifespanpattern for startup/shutdown - [ ] Can write a basic
TestClienttest - [ ] Can articulate the auth dependency chain (token → user → role)
🔑 FastAPI One-Page Cheat Sheet
from fastapi import FastAPI, HTTPException, Depends, Query, Path, status, BackgroundTasks
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field, field_validator
from typing import Optional, List
app = FastAPI()
# CORS
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# Pydantic model
class Item(BaseModel):
name: str = Field(min_length=1)
price: float = Field(gt=0)
tags: List[str] = []
# Dependency
def get_db():
db = connect()
try:
yield db
finally:
db.close()
# Routes
@app.get("/items/", response_model=List[Item])
@app.get("/items/{id}", response_model=Item)
@app.post("/items/", response_model=Item, status_code=201)
@app.put("/items/{id}", response_model=Item)
@app.patch("/items/{id}", response_model=Item)
@app.delete("/items/{id}", status_code=204)
# Key patterns
# raise HTTPException(status_code=404, detail="Not found")
# item.model_dump(exclude_unset=True) ← for PATCH
# background_tasks.add_task(fn, arg)
# Depends(get_db), Depends(get_current_user)
You're ready. Go ship something. 🚀