Middleware & CORS¶
Middleware runs before and/or after every request. Use it for logging, timing, auth checks, headers — anything that should apply to all endpoints.
How middleware fits in the request lifecycle¶
Client request
↓
Middleware 1 (pre)
↓
Middleware 2 (pre)
↓
Your endpoint runs
↓
Middleware 2 (post)
↓
Middleware 1 (post)
↓
Response to client
Each middleware wraps the next. Order matters.
A simple custom middleware¶
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time(request: Request, call_next):
start = time.time()
response = await call_next(request)
elapsed = time.time() - start
response.headers["X-Process-Time"] = f"{elapsed:.4f}"
return response
@app.get("/")
def root():
return {"message": "Hello"}
Every response now includes an X-Process-Time header — even endpoints you wrote before adding the middleware.
Logging middleware¶
@app.middleware("http")
async def log_requests(request: Request, call_next):
print(f"→ {request.method} {request.url.path}")
response = await call_next(request)
print(f"← {response.status_code}")
return response
Bearer-token middleware¶
from fastapi.responses import JSONResponse
@app.middleware("http")
async def check_token(request: Request, call_next):
# Skip public paths
if request.url.path in ("/", "/docs", "/openapi.json"):
return await call_next(request)
auth = request.headers.get("Authorization", "")
if not auth.startswith("Bearer "):
return JSONResponse({"error": "missing token"}, status_code=401)
return await call_next(request)
For per-endpoint auth, prefer Depends(get_current_user) from the previous chapter. Middleware is for "every request, no exceptions" rules.
CORS — Cross-Origin Resource Sharing¶
If your API runs on api.example.com and your front-end on app.example.com, the browser blocks the calls by default. CORS headers tell the browser "yes, this front-end is allowed."
FastAPI ships with a CORS middleware — just add it:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # local dev (Next.js / React)
"https://app.example.com", # production frontend
],
allow_credentials=True, # allow cookies / auth headers
allow_methods=["*"], # or restrict: ["GET", "POST"]
allow_headers=["*"],
)
Common mistake: allow_origins=["*"] + allow_credentials=True is REJECTED by the spec. If you need credentials, list explicit origins.
Test CORS¶
Run the server, then in a browser console:
If CORS is misconfigured, the browser shows an error in the console. If it works, you'll see the JSON.
Other built-in middlewares¶
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.middleware.httpsredirect import HTTPSRedirectMiddleware
# Compress responses larger than 1 KB
app.add_middleware(GZipMiddleware, minimum_size=1000)
# Reject requests with bad Host header
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["api.example.com", "*.example.com"],
)
# Force HTTPS in production
app.add_middleware(HTTPSRedirectMiddleware)
Order of middleware matters¶
Middleware added last runs first (LIFO):
app.add_middleware(MiddlewareA) # added first → runs LAST
app.add_middleware(MiddlewareB) # runs FIRST
app.add_middleware(MiddlewareC) # runs even earlier (outermost)
So in this order:
Typical recommendation: - TrustedHost (outermost, first thing checked). - HTTPSRedirect. - CORS. - GZip. - Custom (logging, timing).
How middleware actually executes¶
1. Request comes in
2. Outermost middleware's pre-block runs
3. It calls `await call_next(request)` — this proceeds inward
4. Next middleware repeats steps 2-3
5. Innermost finishes — your endpoint runs
6. Response bubbles back through the middlewares (post-blocks run)
7. Outermost finishes, response sent
Each middleware is async and uses call_next(request) to pass control inward.
Mounting sub-apps¶
You can mount entirely separate FastAPI apps under a path:
from fastapi import FastAPI
main_app = FastAPI()
metrics_app = FastAPI()
@metrics_app.get("/")
def metrics():
return {"uptime": "100%"}
main_app.mount("/metrics", metrics_app)
Hit /metrics/ to reach the metrics app. Useful for split docs or isolated middleware stacks.
Routers — split your code¶
For real apps, don't put 50 endpoints in main.py. Use APIRouter:
# routers/users.py
from fastapi import APIRouter, Depends
from db import get_db
router = APIRouter(prefix="/users", tags=["users"])
@router.get("/")
def list_users(db = Depends(get_db)):
...
@router.post("/")
def create_user(...):
...
# main.py
from fastapi import FastAPI
from routers import users, orders, auth
app = FastAPI()
app.include_router(users.router)
app.include_router(orders.router)
app.include_router(auth.router)
Each router can have its own dependencies and tags. Tags group endpoints in /docs.
Common pitfalls¶
- ❗ CORS with
*and credentials — invalid. List explicit origins. - ❗ Forgetting
await call_next(request)— middleware that doesn't callcall_nextshort-circuits the request. Sometimes that's intentional (e.g., reject early), but usually it's a bug. - ❗ Middleware that does heavy work — runs on EVERY request. Keep it light or fast.
- ❗ Modifying
requestbody in middleware — possible but tricky because the body can only be read once. For request-body changes, prefer dependencies or transforms in the endpoint. - ❗ Order surprises — added last = runs first. Test the order if you depend on it.
What's next¶
Practice¶
What does this print?
Expected: True
Restrict CORS to specific origins in production (not allow_origins=['*'])
Expected: True
Quiz — Quick check¶
What you remember
Q1. What's middleware?
- Code that runs around every request — before the handler (e.g., auth) and after (e.g., logging)
- A database
- A specific Python library
- An LLM concept
Why: Middleware wraps the request/response cycle. Add behaviors globally without modifying every handler — auth checks, logging, custom headers, timing, CORS.
Q2. Why is allow_origins=["*"] risky in production?
- Any website can call your API from a user's browser, including malicious sites trying CSRF-style attacks
- Slower
- Not actually risky
- Required for HTTPS
Why: Restrict to known frontend domains. The browser enforces CORS based on what your server says it allows.
*opens the door wide.
Q3. What does middleware order matter for?
- Earlier middleware sees the raw request first; later middleware operates on responses first when unwinding
- Doesn't matter
- Performance only
- Security only
Why: Think of it like a stack.
app.add_middleware(A); app.add_middleware(B)means: request goes A→B→handler→B→A→response. Auth typically goes first (innermost reject), logging last (outermost wrap).
Common doubts¶
When should I write custom middleware vs a dependency?
Middleware for cross-cutting concerns affecting all routes: logging, timing, CORS, security headers. Dependency for per-route logic: auth checks, rate limits per endpoint, parameter parsing. Middleware can't access route info; dependencies can.
How do I add a custom HTTP header to every response?
Middleware. @app.middleware("http") decorator: call response = await call_next(request), then response.headers["X-Custom"] = "value", return response. Or use Starlette's BaseHTTPMiddleware for a class-based approach.
What about middleware that does heavy work — does it slow down every request?
Yes. Keep middleware lightweight. For heavy computation, factor into a dependency only routes that need it, or do it async without blocking. Tracing middleware (e.g., Sentry) is usually fine; ML inference middleware is not.