Advanced FastAPI Patterns • Lesson 5

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:

  1. Incoming Request - The request enters the middleware stack
  2. Pre-processing - Your middleware code runs before call_next()
  3. Route Handling - The request is dispatched to the matching endpoint
  4. Post-processing - Your middleware code runs after call_next() returns
  5. 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 incoming Request object
  • call_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

PatternPurpose
TimingMeasure and report request duration
LoggingLog request method, path, and status code
AuthenticationVerify tokens or API keys globally
CORSHandle cross-origin resource sharing
Error HandlingCatch exceptions and return structured errors
Request IDAttach a unique identifier to each request

Key Concepts

  • @app.middleware("http") - Registers an HTTP middleware function
  • call_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; use time.perf_counter() when precision matters
  • Follow the X- prefix convention for custom proprietary headers
  • Always return the response from your middleware; forgetting return response will 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