Python FastAPI
FastAPI web framework patterns for building APIs.
App Creation and Lifespan
Minimal FastAPI app with lifespan event for startup/shutdown resources
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# Startup: initialize DB pool, load ML model, etc.
app.state.db = await create_pool()
yield
# Shutdown: close connections, flush buffers
await app.state.db.close()
app = FastAPI(
title="domus-api",
version="0.1.0",
lifespan=lifespan,
)
Route Decorators
GET, POST, PUT, DELETE with status codes and response models
from fastapi import FastAPI, status
from pydantic import BaseModel
class Device(BaseModel):
hostname: str
ip: str
vlan: int
class DeviceOut(BaseModel):
id: int
hostname: str
ip: str
@app.get("/devices", response_model=list[DeviceOut])
async def list_devices():
return await db.fetch_all("SELECT * FROM devices")
@app.post("/devices", response_model=DeviceOut, status_code=status.HTTP_201_CREATED)
async def create_device(device: Device):
return await db.insert(device.model_dump())
@app.put("/devices/{device_id}", response_model=DeviceOut)
async def update_device(device_id: int, device: Device):
return await db.update(device_id, device.model_dump())
@app.delete("/devices/{device_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_device(device_id: int):
await db.delete(device_id)
Path and Query Parameters
Path params are positional, query params are keyword with defaults
from fastapi import Query
@app.get("/devices/{device_id}")
async def get_device(device_id: int):
"""Path param -- FastAPI validates type automatically."""
...
@app.get("/devices")
async def search_devices(
vlan: int | None = None,
hostname: str | None = Query(None, min_length=3, max_length=64),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
):
"""Query params -- all optional with validation constraints."""
...
Request Body with Pydantic
Pydantic model as request body — FastAPI deserializes and validates automatically
from pydantic import BaseModel, Field
class PolicyUpdate(BaseModel):
name: str = Field(..., min_length=1, max_length=128, description="Policy set name")
vlan: int = Field(..., ge=1, le=4094, description="Target VLAN ID")
enabled: bool = Field(True, description="Whether policy is active")
@app.put("/policies/{policy_id}")
async def update_policy(policy_id: int, body: PolicyUpdate):
# body is already validated -- access fields directly
return {"id": policy_id, **body.model_dump()}
Dependency Injection
Dependencies for shared logic — DB sessions, auth, config
from fastapi import Depends, Header, HTTPException
async def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
async def verify_api_key(x_api_key: str = Header(...)):
if x_api_key != settings.api_key:
raise HTTPException(status_code=403, detail="Invalid API key")
@app.get("/secure-data", dependencies=[Depends(verify_api_key)])
async def secure_endpoint(db: Session = Depends(get_db)):
return db.query(Data).all()
Error Handling
HTTPException for client errors, exception handlers for custom responses
from fastapi import HTTPException, Request
from fastapi.responses import JSONResponse
@app.get("/devices/{device_id}")
async def get_device(device_id: int):
device = await db.get(device_id)
if not device:
raise HTTPException(
status_code=404,
detail=f"Device {device_id} not found",
)
return device
class DeviceNotFoundError(Exception):
def __init__(self, device_id: int):
self.device_id = device_id
@app.exception_handler(DeviceNotFoundError)
async def device_not_found_handler(request: Request, exc: DeviceNotFoundError):
return JSONResponse(
status_code=404,
content={"detail": f"Device {exc.device_id} not found"},
)
Middleware and CORS
CORS for browser clients, custom middleware for logging/timing
import time
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://docs.domusdigitalis.dev"],
allow_methods=["*"],
allow_headers=["*"],
)
@app.middleware("http")
async def add_timing_header(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
elapsed = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{elapsed:.4f}"
return response
Testing with TestClient
Synchronous test client — no event loop needed, works with pytest directly
from fastapi.testclient import TestClient
client = TestClient(app)
def test_list_devices():
response = client.get("/devices")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_create_device():
payload = {"hostname": "switch-01", "ip": "10.50.1.100", "vlan": 10}
response = client.post("/devices", json=payload)
assert response.status_code == 201
data = response.json()
assert data["hostname"] == "switch-01"
def test_not_found():
response = client.get("/devices/99999")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
APIRouter for Modular Apps
Split routes into modules — mount routers with prefixes and tags
from fastapi import APIRouter
router = APIRouter(prefix="/devices", tags=["devices"])
@router.get("/")
async def list_devices():
...
@router.get("/{device_id}")
async def get_device(device_id: int):
...
# In main.py
app.include_router(router)