Custom Middleware & Request Processing
Learn how to build custom middleware in FastAPI to intercept and process requests and responses, adding headers, logging, and timing information.
🎯 What You'll Learn
- •Understand the middleware execution lifecycle in FastAPI
- •Create custom middleware using the @app.middleware decorator
- •Add custom response headers for request timing
- •Apply middleware patterns for logging and monitoring
Custom Middleware & Request Processing
What You'll Learn
- Understand what middleware is and how it fits into the request/response lifecycle
- Build custom middleware using the
@app.middleware("http")decorator - Add timing headers to track request processing duration
- Apply common middleware patterns for logging and monitoring
Theory
Middleware is code that runs for every request before it reaches your endpoint handler, and for every response before it is sent back to the client. Think of middleware as a pipeline that wraps around your application logic.
The Middleware Lifecycle
When a request arrives at your FastAPI application, it flows through middleware in this order:
- Incoming Request - The request enters the middleware stack
- Pre-processing - Your middleware code runs before
call_next() - Route Handling - The request is dispatched to the matching endpoint
- Post-processing - Your middleware code runs after
call_next()returns - Outgoing Response - The modified response is sent to the client
Client -> Middleware (pre) -> Endpoint -> Middleware (post) -> Client
Creating Middleware in FastAPI
FastAPI uses the @app.middleware("http") decorator to register middleware functions. Each middleware function receives two arguments:
request- The incomingRequestobjectcall_next- A function that passes the request to the next middleware or endpoint
@app.middleware("http")
async def my_middleware(request: Request, call_next):
# Pre-processing: runs before the endpoint
print(f"Request to: {request.url}")
response = await call_next(request)
# Post-processing: runs after the endpoint
print(f"Response status: {response.status_code}")
return response
Middleware Execution Order
When you register multiple middleware functions, they execute in reverse order of registration. The last middleware registered is the outermost layer:
@app.middleware("http")
async def middleware_a(request, call_next):
print("A: before")
response = await call_next(request)
print("A: after")
return response
@app.middleware("http")
async def middleware_b(request, call_next):
print("B: before")
response = await call_next(request)
print("B: after")
return response
# Execution order: B before -> A before -> endpoint -> A after -> B after
Timing Middleware
One of the most common middleware patterns is measuring request processing time:
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
response = await call_next(request)
process_time = time.time() - start_time
response.headers["X-Process-Time"] = str(round(process_time, 4))
return response
This pattern captures the time before and after the request is processed, then attaches the duration as a custom response header.
Common Middleware Patterns
| Pattern | Purpose |
|---|---|
| Timing | Measure and report request duration |
| Logging | Log request method, path, and status code |
| Authentication | Verify tokens or API keys globally |
| CORS | Handle cross-origin resource sharing |
| Error Handling | Catch exceptions and return structured errors |
| Request ID | Attach a unique identifier to each request |
Key Concepts
@app.middleware("http")- Registers an HTTP middleware functioncall_next(request)- Forwards the request to the next handler in the chain- Pre-processing - Code before
call_next()runs for every incoming request - Post-processing - Code after
call_next()runs for every outgoing response - Custom Headers - Use
response.headers["X-Header-Name"]to add headers - Request Object - Access
request.method,request.url,request.headers, etc.
Best Practices
- Keep middleware lightweight to avoid adding latency to every request
- Use
time.time()for general timing; usetime.perf_counter()when precision matters - Follow the
X-prefix convention for custom proprietary headers - Always return the response from your middleware; forgetting
return responsewill break the chain - Order your middleware carefully: authentication should run before business logic middleware
- Avoid heavy I/O operations in middleware since they affect every single request
- Use middleware for cross-cutting concerns that apply to all or most endpoints
- For endpoint-specific logic, prefer dependencies over middleware
Additional Resources
💡 Hint
Use @app.middleware('http') to create middleware. The call_next function passes the request to the next handler. You can modify the response after awaiting call_next.
Ready to Practice?
Now that you understand the theory, let's put it into practice with hands-on coding!
Start Interactive Lesson