Skip to content

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:

fetch("http://127.0.0.1:8000/").then(r => r.json()).then(console.log)

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:

Request → C → B → A → endpoint → A → B → C → Response

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 call call_next short-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 request body 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

# Middleware runs for EVERY request — useful for logging, auth, headers
runs_per_request = True
print(runs_per_request)

Restrict CORS to specific origins in production (not allow_origins=['*'])

Expected: True

origins = ["*"]                 # bug: allows ANY domain — security risk in production
is_safe = origins != ["*"] and all(o != "*" for o in origins)
print(not is_safe)

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.

Authentication — OAuth2 + JWT