Timing headers, request ids and CORS are cross-cutting concerns that belong in one place, not in every endpoint.
Middleware wraps the whole application: it sees every request before any route runs and every response before it leaves. The quickest form is a decorator:
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
response.headers["X-Process-Time"] = f"{time.perf_counter() - start:.4f}"
return responsecall_next passes the request down the stack and returns the response. Middleware added later wraps the earlier ones, so the last one added runs first on the way in. Keep middleware for truly global concerns: request ids, timing, security headers, GZipMiddleware. Per-route logic belongs in dependencies, which see validated parameters and appear in the docs.
CORS (Cross-Origin Resource Sharing) is the middleware almost everyone needs. A page served from https://app.example.com calling https://api.example.com is cross-origin, and the browser won't let the page read the response unless the API answers with matching Access-Control-Allow-* headers. For requests with JSON bodies, custom headers or methods like PUT and DELETE, the browser first sends an OPTIONS preflight. CORSMiddleware handles all of it:
allow_origins: exact origins, scheme + host + port, no path or trailing slash.allow_credentials=Trueif the browser sends cookies or Authorization headers. The CORS rules forbid a wildcard origin together with credentials, so list origins explicitly.allow_methodsandallow_headersallow very little by default;["*"]is common.
CORS is a browser rule, not protection for your API: curl, scripts and servers ignore it entirely. Authentication still has to guard your endpoints.
Example
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.testclient import TestClient
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.example.com", "http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/data")
def data():
return {"ok": True}
c = TestClient(app)
pre = c.options("/data", headers={"Origin": "http://localhost:5173",
"Access-Control-Request-Method": "GET"})
print(pre.status_code, pre.headers["access-control-allow-origin"])
# 200 http://localhost:5173
other = c.get("/data", headers={"Origin": "https://evil.example"})
print("access-control-allow-origin" in other.headers)
# False: the browser won't let that page read the responseRemember
- Middleware wraps every request; the last added runs first on the way in
- Use middleware for global concerns, dependencies for per-route logic
- CORSMiddleware answers preflights and adds Access-Control-Allow-* headers
- CORS only constrains browsers; it is not authentication
Watch out for
- Adding a trailing slash or path to an origin (https://app.example.com/), which never matches.