Every production APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. has logic that applies to all requests: logging, timing, authenticationWhat is authentication?Verifying who a user is, typically through credentials like a password or token., request IDs. Writing this logic inside every route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes. would be repetitive and error-prone. MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. solves this by wrapping your entire application in layers that process requests on the way in and responses on the way out.
If you have used Express middleware before, FastAPI middleware is conceptually identical, but the mechanics differ because FastAPI runs on ASGI, Python's async server standard.
How ASGI middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. works
ASGI (Asynchronous Server Gateway Interface) is the protocolWhat is protocol?An agreed-upon set of rules for how two systems communicate, defining the format of messages and the expected sequence of exchanges. that connects your FastAPI application to the web server. Every request flows through a stack of middleware layers before reaching your route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.. Each layer can inspect or modify the request, decide whether to continue, and then inspect or modify the response on the way back out.
Think of it as a series of concentric shells around your application. The outermost shell sees the request first and the response last. The innermost shell sees the request last and the response first.
Request → Middleware A → Middleware B → Route Handler
Response ← Middleware A ← Middleware B ← Route HandlerThis is sometimes called the "onion model", the request peels through layers going in, and the response builds them back going out.
The @app.middleware("http") pattern
FastAPI provides a simple decorator for adding middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.. Every middleware function receives two arguments: the incoming request and a call_next function that passes the request to the next layer.
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
print(f"Incoming: {request.method} {request.url.path}")
response = await call_next(request)
print(f"Completed: {response.status_code}")
return responseThe key line is response = await call_next(request). This is where your middleware hands control to the next layer (and eventually to your route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.). Everything before this line runs on the way in. Everything after runs on the way out.
return response at the end. Without it, FastAPI returns an empty response with no status code, no headers, no body. The client gets a cryptic connection error, and nothing in your logs explains why.Common middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. patterns
Three middleware patterns appear in almost every production FastAPI application. Understanding them helps you recognize what AI is generating and whether it is correct.
Request timing
Measuring how long each request takes is essential for spotting slow endpoints and setting performance baselines.
import time
@app.middleware("http")
async def add_timing(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
response.headers["X-Process-Time"] = f"{duration:.4f}"
return responseNotice that timing middleware measures the total time including all inner middleware layers and the route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes. itself. This is the onion model in action, the outermost layer captures the full duration.
Request ID injection
Every request gets a unique identifier so you can trace it through logs, error reports, and downstream services. This is non-negotiable in production APIs.
import uuid
@app.middleware("http")
async def add_request_id(request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid.uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return responseTwo important details here. First, the middleware respects an existing X-Request-ID header from the client or a load balancerWhat is load balancer?A server that distributes incoming traffic across multiple backend servers so no single server gets overwhelmed., it only generates a new one if none exists. Second, it stores the ID on request.state, which is the correct way to attach custom data to a request in FastAPI. Your route handlers can then access it via request.state.request_id.
Logging middleware
A structured loggingWhat is structured logging?Writing log entries as machine-readable JSON objects with consistent fields instead of plain text, making them searchable by log analysis tools. middleware captures method, path, status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke., and duration in a consistent format.
import logging
import time
logger = logging.getLogger("api")
@app.middleware("http")
async def log_requests(request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
logger.info(
"request_completed",
extra={
"method": request.method,
"path": request.url.path,
"status": response.status_code,
"duration_ms": round(duration * 1000, 2),
"request_id": getattr(request.state, "request_id", "unknown"),
},
)
return responserequest.state object is a simple namespace, you can set any attribute on it. But it only lives for the duration of one request. Each new request gets a fresh request.state.MiddlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. ordering
The order you add middleware matters. FastAPI processes middleware in the order it is registered, but the response travels back through the stack in reverse. This creates a subtle but important rule: middleware added first becomes the outermost layer.
# Registration order
app.add_middleware(CORSMiddleware, ...) # Added first → outermost
app.add_middleware(TimingMiddleware) # Added second → middle
# @app.middleware("http") handlers # Added last → innermostIn practice, the outermost middleware sees the raw request before any other middleware touches it, and it sees the final response after all other middleware have modified it.
Here is the typical ordering for a production FastAPI app:
| Order | Middleware | Why this position |
|---|---|---|
| 1 (outermost) | CORS | Must add CORS headers before anything else might reject the request |
| 2 | Request ID | All subsequent middleware and handlers need the request ID for logging |
| 3 | Timing | Measures everything inside, including auth overhead |
| 4 | Logging | Captures final status code, duration, and request ID |
| 5 (innermost) | Auth | Runs closest to route handlers, can short-circuit early |
OPTIONS request with no credentials, the auth middleware rejects it with 401, and the browser never gets the CORS headers it needs. The result is a confusing CORS error in the browser console that has nothing to do with CORS configuration, it is an ordering bug.The call_next trap
The most dangerous middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. bug is forgetting to call await call_next(request). When this happens, no route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes. executes. The middleware returns whatever response it constructs (or None, which crashes the server).
# BROKEN - never calls call_next
@app.middleware("http")
async def broken_auth(request: Request, call_next):
token = request.headers.get("Authorization")
if not token:
return JSONResponse(
status_code=401,
content={"error": "Not authenticated"},
)
# Missing: response = await call_next(request)
# Missing: return responseThe middleware above correctly rejects unauthenticated requests. But when a valid tokenWhat is token?The smallest unit of text an LLM processes - roughly three-quarters of a word. API pricing is based on how many tokens you use. is present, the function reaches the end without returning anything. The route handler never runs. The client gets an empty response or a server error.
The fix is straightforward:
@app.middleware("http")
async def working_auth(request: Request, call_next):
token = request.headers.get("Authorization")
if not token:
return JSONResponse(
status_code=401,
content={"error": "Not authenticated"},
)
# Token exists - continue to route handler
response = await call_next(request)
return responseapp.add_middleware vs @app.middleware
FastAPI gives you two ways to add middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it., and they serve different purposes.
@app.middleware("http") is for simple, custom middleware functions. It is the pattern you have seen throughout this lesson.
app.add_middleware() is for adding middleware classes, typically third-party ones like CORSMiddleware or Starlette middleware. It accepts the middleware class and its configuration as keyword arguments.
from starlette.middleware.trustedhost import TrustedHostMiddleware
# Class-based middleware (third-party)
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com"],
)
# Function-based middleware (custom)
@app.middleware("http")
async def custom_header(request: Request, call_next):
response = await call_next(request)
response.headers["X-Custom"] = "FastAPI"
return responseThe important difference: middleware added with app.add_middleware() runs before middleware added with @app.middleware("http"). This is why CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it. and other infrastructure concerns use app.add_middleware(), they need to be the outermost layers.
What AI gets right and wrong
When you ask an AI to add middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. to a FastAPI app, it usually generates syntactically correct middleware functions. The patterns (timing, logging, auth) are well-represented in training data. Where AI consistently fails:
- Ordering: AI places middleware in the order you mention it in your prompt, not in the order the application needs it. If you say "add auth and CORSWhat is cors?Cross-Origin Resource Sharing - a browser security rule that blocks web pages from making requests to a different domain unless that domain explicitly allows it.," it often puts auth first.
- Missing
return: About one in five AI-generated middleware functions forget to return the response afterawait call_next(request). - Sync instead of async: AI occasionally generates synchronous middleware (using
definstead ofasync def), which blocks the event loopWhat is event loop?The mechanism that lets Node.js handle many operations on a single thread by delegating slow tasks and processing their results when ready. and silently degrades performance under load. - Redundant middleware: If you ask for logging and timing separately, AI happily creates two middleware functions that both measure duration, doubling the overhead.