Skip to main content

Middleware and CORS

Middleware intercepts every request and response to handle logging, authentication, compression, and CORS.


Basic Middleware​

import time
import uuid
from fastapi import FastAPI, Request, Response

app = FastAPI()


# @app.middleware("http"): handles all HTTP requests and responses
@app.middleware("http")
async def logging_middleware(request: Request, call_next):
# Before request handling
request_id = str(uuid.uuid4())[:8]
start_time = time.perf_counter()

print(f"[{request_id}] → {request.method} {request.url.path}")

# Call the next middleware or route handler
response: Response = await call_next(request)

# After response
elapsed = (time.perf_counter() - start_time) * 1000
print(f"[{request_id}] ← {response.status_code} ({elapsed:.1f}ms)")

# Add response headers
response.headers["X-Request-ID"] = request_id
response.headers["X-Process-Time"] = f"{elapsed:.1f}ms"

return response


@app.get("/items/{item_id}")
def get_item(item_id: int):
return {"item_id": item_id}

CORS Configuration​

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# CORS: allow cross-origin requests from browsers
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000", # development
"https://myapp.com", # production
],
allow_credentials=True, # allow cookies
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
allow_headers=["*"],
)


# Development: allow all origins (DO NOT use in production)
def create_dev_app():
dev_app = FastAPI()
dev_app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
return dev_app


@app.get("/api/data")
def get_data():
return {"data": "cross-origin accessible"}

GZip and TrustedHost Middleware​

from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware

app = FastAPI()

# GZip: compress responses larger than minimum_size
app.add_middleware(GZipMiddleware, minimum_size=1000)

# TrustedHost: only allow specified hosts
app.add_middleware(
TrustedHostMiddleware,
allowed_hosts=["example.com", "*.example.com", "localhost"],
)


@app.get("/large-data")
def large_data():
return {"data": "x" * 5000} # compressed with GZip

Starlette BaseHTTPMiddleware​

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

app = FastAPI()


class RateLimitMiddleware(BaseHTTPMiddleware):
"""Request rate limiting (simulation)"""

def __init__(self, app, max_requests: int = 100):
super().__init__(app)
self.max_requests = max_requests
self._request_counts: dict[str, int] = {}

async def dispatch(self, request: Request, call_next) -> Response:
client_ip = request.client.host if request.client else "unknown"
count = self._request_counts.get(client_ip, 0) + 1
self._request_counts[client_ip] = count

if count > self.max_requests:
return JSONResponse(
status_code=429,
content={"detail": "Too Many Requests"},
)

response = await call_next(request)
response.headers["X-RateLimit-Remaining"] = str(self.max_requests - count)
return response


class SecurityHeadersMiddleware(BaseHTTPMiddleware):
"""Add security headers"""

async def dispatch(self, request: Request, call_next) -> Response:
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["X-XSS-Protection"] = "1; mode=block"
return response


app.add_middleware(RateLimitMiddleware, max_requests=1000)
app.add_middleware(SecurityHeadersMiddleware)


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

Middleware Execution Order​

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def middleware_a(request: Request, call_next):
print("A: before request")
response = await call_next(request)
print("A: after response")
return response


@app.middleware("http")
async def middleware_b(request: Request, call_next):
print("B: before request")
response = await call_next(request)
print("B: after response")
return response


# Execution order (LIFO — last registered runs first):
# B: before request
# A: before request
# [route handler]
# A: after response
# B: after response


@app.get("/")
def root():
print("route handler")
return {}

Summary​

MiddlewarePurpose
@app.middleware("http")Custom HTTP middleware
CORSMiddlewareAllow cross-origin requests
GZipMiddlewareResponse compression
TrustedHostMiddlewareHost whitelist
BaseHTTPMiddlewareStarlette-based custom middleware

Use middleware for processing common to all requests. For per-request logic, prefer dependency injection.