Built-in Middleware

Difficulty: Intermediate

Middleware in FastAPI (and Starlette) is code that runs before every request reaches a route handler and after every response is sent back. It sits between the client and your route handlers, forming a pipeline that processes every request and response. Middleware is ideal for cross-cutting concerns that apply to all (or most) endpoints.

FastAPI inherits Starlette's middleware system and provides several built-in middleware classes. These handle common tasks like CORS, GZip compression, HTTPS redirection, and trusted host validation. You add middleware using app.add_middleware() and they execute in the reverse order they are added (last added runs first).

GZipMiddleware compresses responses that are larger than a minimum size, reducing bandwidth usage. It checks the Accept-Encoding header from the client and compresses with gzip if supported. This is especially useful for JSON API responses that can be compressed significantly.

TrustedHostMiddleware validates that requests come from allowed host names. This prevents HTTP Host header attacks where an attacker sends requests with a forged Host header. You specify a list of allowed hosts, and requests with other Host headers are rejected with a 400 error.

HTTPSRedirectMiddleware redirects all HTTP requests to HTTPS. This is useful in production to ensure all traffic is encrypted. In development, you typically do not use this middleware since you are using HTTP locally.

Middleware execution order matters. Middleware runs in a stack: the first middleware added wraps the outermost layer. When a request comes in, it passes through middleware from outside to inside (last added first), reaches the handler, and the response passes back through middleware from inside to outside (first added last). Understanding this order is important when middleware depends on other middleware's processing.

Code examples

Adding Built-in Middleware

from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
from starlette.middleware.trustedhost import TrustedHostMiddleware
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware

app = FastAPI()

# Compress responses larger than 500 bytes
app.add_middleware(GZipMiddleware, minimum_size=500)

# Only allow requests from these hosts
app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["example.com", "*.example.com", "localhost"],
)

# In production: redirect HTTP to HTTPS
# app.add_middleware(HTTPSRedirectMiddleware)

@app.get("/")
def root():
    return {"message": "Hello with middleware"}

Built-in middleware is added via app.add_middleware(). GZipMiddleware compresses large responses. TrustedHostMiddleware validates the Host header. Each middleware has configuration options passed as keyword arguments.

Middleware Execution Order

from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response

app = FastAPI()

class MiddlewareA(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        print("A: before request")
        response = await call_next(request)
        print("A: after response")
        return response

class MiddlewareB(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        print("B: before request")
        response = await call_next(request)
        print("B: after response")
        return response

# Added in this order:
app.add_middleware(MiddlewareA)  # Added first -> outermost
app.add_middleware(MiddlewareB)  # Added second -> runs first

@app.get("/")
def root():
    print("Handler")
    return {"message": "ok"}

# Request flow:
# B: before request
# A: before request
# Handler
# A: after response
# B: after response

Middleware added last runs first (like a stack). MiddlewareB was added second so it runs first. The request flows inward through the stack, and the response flows outward. This is important when middleware order matters.

Practical Middleware Stack

from fastapi import FastAPI
from starlette.middleware.gzip import GZipMiddleware
from fastapi.middleware.cors import CORSMiddleware
import time

app = FastAPI()

# 1. CORS (outermost - handles preflight before other processing)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_methods=["*"],
    allow_headers=["*"],
)

# 2. GZip compression
app.add_middleware(GZipMiddleware, minimum_size=1000)

# 3. Custom timing middleware (innermost)
@app.middleware("http")
async def add_process_time(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

@app.get("/")
def root():
    return {"message": "Processed through middleware stack"}

A practical middleware stack: CORS handles browser cross-origin requests first, GZip compresses responses, and a custom middleware adds timing headers. The @app.middleware decorator is a shortcut for simple middleware.

Key points

Concepts covered

Middleware, Request Processing, Response Processing, GZip, Trusted Hosts