Middleware is executed in reverse order of registration (last registered = outermost). Register in this order:
## app/main.py
app = FastAPI()
## 1. CORS (outermost — must handle preflight before anything else)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.CORS_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
## 2. Request ID (early — needed by all subsequent middleware and handlers)
app.add_middleware(RequestIDMiddleware)
## 3. Logging (after request ID is set)
app.add_middleware(LoggingMiddleware)
## 4. Rate limiting (before business logic)
app.add_middleware(RateLimitMiddleware)- MUST use the
BaseHTTPMiddlewarepattern or pure ASGI middleware - MUST NOT perform database queries in middleware (use dependencies instead)
- MUST handle exceptions gracefully — middleware errors should not crash the app
from starlette.middleware.base import BaseHTTPMiddleware
class RequestIDMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
request_id = request.headers.get("X-Request-ID", str(uuid4()))
request.state.request_id = request_id
response = await call_next(request)
response.headers["X-Request-ID"] = request_id
return response- MUST configure CORS origins from environment variables (never hardcode)
- MUST restrict
allow_originsin production (never use["*"]in production) - MUST allow credentials only when necessary
- See
common/cors.mdfor general CORS principles
- MUST log request method, path, status code, and duration for every request
- MUST include request ID in all log entries
- MUST NOT log request/response bodies (may contain PII)
- See
common/logging.mdfor general logging principles
class LoggingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
start = time.perf_counter()
response = await call_next(request)
duration = time.perf_counter() - start
logger.info(
"request_completed",
method=request.method,
path=request.url.path,
status=response.status_code,
duration_ms=round(duration * 1000, 2),
request_id=getattr(request.state, "request_id", None),
)
return response