Skip to content

FastAPI Middleware

Middleware is code that runs before and after every request processed by your application. It acts as a bridge between the client and your route handlers.

You can use middleware to intercept incoming requests (to log them, check authentication, or modify headers) before they reach your route logic. You can also intercept the outgoing response before it returns to the client.

Building a Middleware

We create middleware using the @app.middleware("http") decorator.

Python
from fastapi import Request


@app.middleware("http")
async def my_middleware(request: Request, call_next):
    # Pre-route work happens here (before the endpoint is called)

    response = await call_next(request)

    # Post-route work happens here (after the endpoint finishes)

    return response

The function takes the incoming Request and a call_next function. The call_next function is what actually passes the request down the chain to your route handler (or to the next middleware). Once your route handler finishes, call_next returns the Response object, allowing you to modify it or log information before returning it to the user.

If we register multiple middlewares, each one wraps the next.

Python
app.add_middleware(First)
# ...
app.add_middleware(Last)

Will be executed in this order:

  • On Requests: Last -> ... -> First -> route
  • On Responses: route -> First -> ... -> Last

Why Middleware Must Be async

FastAPI runs requests on an asyncio event loop. call_next(request) returns a coroutine that needs to be awaited to actually run the downstream pipeline (the next middleware, the route handler, the response build). await is only legal inside an async def function. A synchronous middleware would have no way to call call_next and get the Response back.

@app.exception_handler doesn't have that constraint, since handlers don't await anything by default. They can be sync or async.

A Logging Middleware

Let's use our new logging knowledge to create a middleware that measures how long every request takes.

src/release_tracker/main.py
import logging
import time

from fastapi import Request

logger = logging.getLogger(__name__)

# ... app initialization ...


@app.middleware("http")
async def log_requests(request: Request, call_next):
    start_time = time.perf_counter()
    response = await call_next(request)
    process_time = (time.perf_counter() - start_time) * 1000

    logger.info(
        "%s %s completed in %.2fms with status code %s",
        request.method,
        request.url.path,
        process_time,
        response.status_code,
    )
    return response

Lazy formatting (%s) shows up here too. Every HTTP request the app handles now produces a timing log line, without a single logging call inside the route handlers themselves.


Other Common Middlewares

While we wrote our own custom middleware, you'll frequently see two other types in production codebases:

  1. CORSMiddleware:
    • This is registered via app.add_middleware(CORSMiddleware, ...).
    • It tells the browser that if our API is hosted on api.example.com and our frontend on a different origin (like example.com) the frontend is allowed to call our API.
    • A discussion on CORS is out of scope.
  2. @app.exception_handler:
    • A specialized handler that intercepts requests when a specific exception type is raised.

Async vs Sync Handlers

You might notice that our middleware was an async def function, but our exception handler is a regular synchronous def function.

In FastAPI, exception handlers can be either! You only need to make a handler async if you are performing blocking I/O inside it (like making a network request or reading a large file). Since our handle_integrity_error simply returns an in-memory JSONResponse object, a synchronous function is perfectly fine and slightly more efficient.