Middleware is code that wraps every request and response. Think of it like a security guard at a door — every visitor passes through, the guard can inspect, modify, or block them, then lets them through to the actual room. Logging, timing, auth headers, request IDs — all classic middleware jobs.
Custom middleware with @app.middleware("http")
The decorator gives us the simplest API. We receive the request, call the next handler with await call_next(request), get the response, and return it.
import time
import uuid
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start = time.perf_counter()
request_id = str(uuid.uuid4())
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
response.headers["X-Request-ID"] = request_id
return response
Middlewares run in reverse order of registration on the way back out. Last added = outermost.
CORS — the browser security thing
Browsers block JavaScript from https://app.com calling https://api.com unless api.com says it’s OK. That’s the same-origin policy. CORS is how the server says “yes, this origin is allowed”.
Without CORS configured, our React app on localhost:3000 cannot call our API on localhost:8000. Browser blocks it before the request even gets a response payload.
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.pman47.cc", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Notes:
- Never use
allow_origins=["*"]withallow_credentials=True. Browsers reject that combo, and it’s a security footgun. allow_methods=["*"]is fine for development. In prod, list them explicitly:["GET", "POST", "PUT", "DELETE"].- CORS only matters for browsers.
curland server-to-server calls completely ignore CORS.
Other common built-in middlewares
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(TrustedHostMiddleware, allowed_hosts=["pman47.cc", "*.pman47.cc"])
app.add_middleware(HTTPSRedirectMiddleware)
Middleware vs Dependencies — when to use which
This trips a lot of people up.
| Middleware | Dependency | |
|---|---|---|
| Scope | Every request, no exceptions | Only routes that declare it |
| Access to path params | No (path isn’t matched yet) | Yes |
| Can return early | Yes (return a Response directly) | Yes (raise HTTPException) |
| Best for | Logging, timing, CORS, gzip, request ID | Auth, DB sessions, permission checks |
In simple language: middleware is for things every request needs (logging, CORS). Dependencies are for things specific routes need (this endpoint requires a logged-in user).
For auth specifically, dependencies are usually the better call — they show up in OpenAPI docs, work with the /docs Authorize button, and let us mix protected and public routes cleanly.
Order matters
app.add_middleware(CORSMiddleware, ...) # added second → outer
app.add_middleware(GZipMiddleware, ...) # added first → inner
CORS should usually be outermost so it handles preflight OPTIONS requests before anything else. If our auth middleware runs before CORS, browser preflights get a 401 and the real request never fires.
Interview cheat sheet
- Middleware = code wrapping every request. Use
@app.middleware("http")orapp.add_middleware(...). - CORS = browser thing. Configure
CORSMiddlewarewith explicit origins for prod. - Middleware vs dependencies: middleware for cross-cutting concerns, dependencies for per-route logic.
- Order matters — last added = outermost. CORS should be outer.
- Curl and server-side clients don’t care about CORS.