FastAPI/
Lesson

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 Handler

This is sometimes called the "onion model", the request peels through layers going in, and the response builds them back going out.

02

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 response

The 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.

AI pitfall
AI-generated middleware often forgets the 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.
03

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 response

Notice 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 response

Two 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 response
Good to know
The request.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.
04

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   → innermost

In 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:

OrderMiddlewareWhy this position
1 (outermost)CORSMust add CORS headers before anything else might reject the request
2Request IDAll subsequent middleware and handlers need the request ID for logging
3TimingMeasures everything inside, including auth overhead
4LoggingCaptures final status code, duration, and request ID
5 (innermost)AuthRuns closest to route handlers, can short-circuit early
AI pitfall
AI frequently places authentication middleware before CORS middleware. This breaks preflight requests: the browser sends an 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.
05

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 response

The 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 response
06

app.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 response

The 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.

07

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 after await call_next(request).
  • Sync instead of async: AI occasionally generates synchronous middleware (using def instead of async 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.