Cheatsheet for middleware. Combine with the FastAPI textbook .
Built-in middleware
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
from starlette.middleware.sessions import SessionMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://yourdomain.com"],
allow_credentials=True,
allow_methods=["*"], allow_headers=["*"],
expose_headers=["x-request-id"],
max_age=600,
)
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["yourdomain.com", "*.yourdomain.com"])
app.add_middleware(HTTPSRedirectMiddleware)
app.add_middleware(SessionMiddleware, secret_key="...", max_age=86400, https_only=True, same_site="lax")
Order: middleware added LAST runs FIRST (outer-most). Add in expected outer-to-inner order.
Function-style HTTP middleware
@app.middleware("http")
async def add_x_process_time(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{(time.perf_counter() - start) * 1000:.1f}ms"
return response
Note: @app.middleware("http") doesn’t support WebSockets — use ASGI middleware for those.
Request ID
@app.middleware("http")
async def request_id(request: Request, call_next):
rid = request.headers.get("x-request-id") or uuid.uuid4().hex
request.state.request_id = rid
resp = await call_next(request)
resp.headers["x-request-id"] = rid
return resp
Tenant resolution
@app.middleware("http")
async def tenant(request: Request, call_next):
tid = request.headers.get("x-tenant-id")
if tid: request.state.tenant_id = int(tid)
return await call_next(request)
Security headers
@app.middleware("http")
async def security_headers(request, call_next):
resp = await call_next(request)
resp.headers["X-Content-Type-Options"] = "nosniff"
resp.headers["X-Frame-Options"] = "DENY"
resp.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
resp.headers["Strict-Transport-Security"] = "max-age=63072000; includeSubDomains; preload"
return resp
Rate limit (slowapi)
from slowapi import Limiter
from slowapi.errors import RateLimitExceeded
from slowapi.middleware import SlowAPIMiddleware
limiter = Limiter(key_func=lambda req: req.client.host)
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
app.add_middleware(SlowAPIMiddleware)
@app.get("/")
@limiter.limit("60/minute")
async def root(request: Request): ...
Body inspection (be careful!)
@app.middleware("http")
async def log_bodies(request: Request, call_next):
body = await request.body()
log.info("body", b=body[:1024])
# Need to put it back if you read it:
request._body = body
return await call_next(request)
Reading body once consumes the stream. For large bodies: don’t. Use logging at handler level.
Custom ASGI middleware (HTTP + WebSocket)
class MyMiddleware:
def __init__(self, app):
self.app = app
async def __call__(self, scope, receive, send):
if scope["type"] not in ("http", "websocket"):
return await self.app(scope, receive, send)
# before
async def send_wrapper(message):
# mutate response start if needed
await send(message)
await self.app(scope, receive, send_wrapper)
app.add_middleware(MyMiddleware)
Handles both HTTP and WS.
Compression at proxy vs app
For K8s with nginx-ingress: gzip at ingress. Skip GZipMiddleware in app. For direct serve: keep GZipMiddleware.
Order matters
[Outer] CORS
GZip
TrustedHost
RequestID
Tenant
Auth
[Inner] Handler
CORS outermost (preflights need OPTIONS responses without auth).
Read this next
If you want my middleware stack starter, it’s at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .