FastAPI
Modern async Python web framework, built on Starlette (ASGI) + [[Pydantic]]. Auto-generates OpenAPI docs from type hints — the schema isn't written separately from the code, it's derived from it.
Minimal App
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI(title="My API", version="1.0.0")
class User(BaseModel):
name: str
email: str
@app.get("/users/{user_id}")
async def get_user(user_id: int) -> User:
return User(name="Alice", email="alice@example.com")
@app.post("/users", status_code=201)
async def create_user(user: User) -> User:
return user # `user` arrives already validated — Pydantic ran before this function body executes
/docs serves an interactive Swagger UI generated directly from these type hints — the documentation cannot drift from the actual accepted/returned shapes, since both are the same source of truth.
Path, Query, Body Parameters
from fastapi import Query, Path
@app.get("/items/{item_id}")
async def get_item(item_id: int = Path(gt=0)): # path param, validated > 0
...
@app.get("/items")
async def list_items(skip: int = Query(0, ge=0), limit: int = Query(10, le=100)):
... # query params, cursor-style pagination — see API Design Principles
class CreateItem(BaseModel):
name: str
price: float
@app.post("/items")
async def create_item(item: CreateItem): # request body — parsed + validated as a Pydantic model
...
Response Models — Shaping What Goes Out, Not Just What Comes In
class UserResponse(BaseModel):
id: int
name: str
# deliberately no password field
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: int):
user = await db.get_user(user_id) # the DB object might have a password_hash field
return user # FastAPI strips anything not in UserResponse automatically
Why this matters beyond convenience: response_model isn't just validation, it's an authorization boundary for output shape — even if the returned object happens to carry a sensitive field, the response model strips it before serialization. Forgetting to define a narrow response model (returning the raw DB object type instead) is a real, checkable way to accidentally leak fields like password hashes or internal IDs.
Dependency Injection — the Framework's Standout Feature
from fastapi import Depends
async def get_db() -> AsyncSession:
async with SessionLocal() as session:
yield session # code after yield runs as cleanup, once the request finishes
async def get_current_user(token: str = Depends(oauth2_scheme), db: AsyncSession = Depends(get_db)) -> User:
user = await verify_token(token, db)
if not user:
raise HTTPException(status_code=401, detail="Invalid token")
return user
@app.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
return current_user
Dependencies chain automatically — get_current_user declares it needs get_db, and FastAPI resolves the whole chain without the route handler needing to know or construct any of it. This is the same dependency-inversion principle as [[SOLID Principles]]'s "D" — routes depend on an abstraction (Depends(...)), not a concrete session/user-lookup implementation, which is what makes swapping in a mock database session for tests trivial.
Middleware
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import time
app.add_middleware(CORSMiddleware, allow_origins=["https://myapp.com"], allow_methods=["*"])
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
start = time.time()
response = await call_next(request)
response.headers["X-Process-Time"] = str(time.time() - start)
return response
app.add_middleware(TimingMiddleware)
This is the same [[API Gateway]] cross-cutting-concerns pattern (auth, logging, timing applied uniformly across every route) implemented at the single-service level rather than at a fleet-wide gateway.
Background Tasks
from fastapi import BackgroundTasks
def send_email(email: str, message: str):
email_client.send(email, message) # runs AFTER the response is already sent to the client
@app.post("/register")
async def register(user: CreateUser, background_tasks: BackgroundTasks):
created = await db.create_user(user)
background_tasks.add_task(send_email, user.email, "Welcome!")
return created # client gets their response immediately; email send happens after, off the critical path
This is a lightweight, in-process version of the async-decoupling idea behind [[Message Queues & Kafka]] — fine for a best-effort side effect within a single process, but note it has none of a real queue's durability (a process crash after the response is sent, before the background task runs, silently drops it) — worth naming that trade-off if asked about it.
Routers — Modular Structure
# routers/users.py
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
async def list_users(): ...
# main.py
app.include_router(users.router)
Error Handling
class AppError(Exception):
def __init__(self, message: str, code: int = 400):
self.message, self.code = message, code
@app.exception_handler(AppError)
async def app_error_handler(request, exc: AppError):
return JSONResponse(status_code=exc.code, content={"error": exc.message})
A centralized exception handler is what keeps every route from needing its own repetitive try/except — matching [[API Design Principles]]'s consistent error-response-format requirement without duplicating the formatting logic per endpoint.
Lifespan Events
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app: FastAPI):
await db.connect() # startup
yield
await db.disconnect() # shutdown
app = FastAPI(lifespan=lifespan)
This is the exact same @contextmanager-generator pause/resume pattern discussed in [[Python Generators & Iterators]] — code before yield is setup, code after is guaranteed cleanup, applied here to the whole application's lifecycle rather than a single with block.
Interview Drills
- Why does defining a narrow
response_modelmatter beyond input validation, when the input was never a concern for a GET request? — It also governs output shape — FastAPI strips any field on the returned object that isn't declared on the response model, which is a real security boundary (e.g., preventing an internalpassword_hashfield from ever being serialized into the API response) even if the underlying DB object happens to carry it. - How does FastAPI's dependency injection make testing easier? — Routes declare dependencies as
Depends(some_function)rather than importing and calling a concrete implementation directly — in tests,app.dependency_overrides[get_db] = get_test_dbswaps in a test double for the whole request without touching route code, the same dependency-inversion benefit named in [[SOLID Principles]]. - A background task added via
BackgroundTasksneeds to send a critical, must-not-be-lost notification. Is this the right tool? — No —BackgroundTasksruns in-process after the response is sent, with no durability guarantee; a process crash between "response sent" and "task executed" silently drops it. A must-not-be-lost side effect belongs on a real durable queue (see [[Message Queues & Kafka]]), not an in-process background task, which is better suited to best-effort side effects like a welcome email that's fine to occasionally miss.
Cross-links
[[Pydantic]] · [[Asyncio]] · [[API Design Principles]] · [[API Gateway]] · [[SOLID Principles]] · [[Message Queues & Kafka]]