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.
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.
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.
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:
CORSMiddleware:- This is registered via
app.add_middleware(CORSMiddleware, ...). - It tells the browser that if our API is hosted on
api.example.comand our frontend on a different origin (likeexample.com) the frontend is allowed to call our API. - A discussion on CORS is out of scope.
- This is registered via
@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.