FastAPI Middleware Introduction
What is Middleware?
Middleware is a software component that sits between the server receiving a request and the route handlers that process it. Think of middleware as a series of functions that run before your route handlers process the request or after your route handlers generate a response.
In FastAPI, middleware allows you to:
- Execute code for every request before it's processed by your route handlers
- Execute code for every response after it's generated by your route handlers
- Modify incoming requests
- Modify outgoing responses
- Handle cross-cutting concerns like authentication, logging, or CORS
How Middleware Works in FastAPI
In FastAPI, middleware is implemented using the ASGI (Asynchronous Server Gateway Interface) specification. When a request comes in, it flows through all registered middleware in the order they were added before reaching your route handlers. After your route handler generates a response, the middleware is executed again in reverse order.
The basic structure of a FastAPI middleware looks like this:
@app.middleware("http")
async def my_middleware(request: Request, call_next):
# Code to run before the request is processed
response = await call_next(request)
# Code to run after the request is processed
return response
Let's break down the components:
@app.middleware("http")
- A decorator that registers a function as middlewarerequest
- The incoming request objectcall_next
- A function that passes the request to the next middleware or route handlerresponse
- The response generated by your route handler or the next middleware
Your First Middleware: Request Timing
Let's create a simple middleware that measures how long it takes to process a request:
from fastapi import FastAPI, Request
import time
app = FastAPI()
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
start_time = time.time()
# Process the request through the next middleware and route handlers
response = await call_next(request)
# Calculate processing time
process_time = time.time() - start_time
# Add a custom header with the processing time
response.headers["X-Process-Time"] = str(process_time)
return response
@app.get("/")
async def root():
return {"message": "Hello World"}
When you send a request to this app, the response will include a header X-Process-Time
showing how long it took to process the request. If you check the response headers, you'll see something like:
X-Process-Time: 0.0023562908172607422
Common Built-in Middleware in FastAPI
FastAPI includes several built-in middleware options:
CORS Middleware
Cross-Origin Resource Sharing (CORS) middleware allows you to control which domains can access your API:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["https://frontend.example.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
return {"message": "Hello World"}
GZip Middleware
This middleware automatically compresses responses using GZip:
from fastapi import FastAPI
from fastapi.middleware.gzip import GZipMiddleware
app = FastAPI()
app.add_middleware(GZipMiddleware, minimum_size=1000)
@app.get("/")
async def root():
return {"message": "Hello World" * 100} # Large response will be compressed
Creating a Custom Authentication Middleware
Let's create a more practical example: a simple authentication middleware that checks for an API key in the request headers:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import JSONResponse
app = FastAPI()
# In a real app, you would store this in a more secure way
API_KEYS = ["secret_key_1", "secret_key_2"]
@app.middleware("http")
async def authenticate(request: Request, call_next):
if request.url.path.startswith("/docs") or request.url.path.startswith("/openapi"):
# Skip authentication for documentation
return await call_next(request)
api_key = request.headers.get("X-API-Key")
if not api_key or api_key not in API_KEYS:
return JSONResponse(
status_code=401,
content={"detail": "Invalid or missing API key"},
)
# Authentication successful, proceed with request
return await call_next(request)
@app.get("/protected")
async def protected_route():
return {"message": "This is a protected route"}
To test this middleware:
-
Send a request without an API key:
curl http://localhost:8000/protected
You'll get:
{"detail":"Invalid or missing API key"}
-
Send a request with a valid API key:
curl -H "X-API-Key: secret_key_1" http://localhost:8000/protected
You'll get:
{"message":"This is a protected route"}
Logging Middleware
Here's another practical example that logs every request to your API:
from fastapi import FastAPI, Request
import logging
import time
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
app = FastAPI()
@app.middleware("http")
async def log_requests(request: Request, call_next):
# Log request info
logger.info(f"Request started: {request.method} {request.url}")
start_time = time.time()
# Process the request
response = await call_next(request)
# Log response info
process_time = time.time() - start_time
logger.info(f"Request completed: {request.method} {request.url} - Status: {response.status_code} - Time: {process_time:.4f}s")
return response
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/slow")
async def slow_route():
time.sleep(1) # Simulate slow processing
return {"message": "This is a slow route"}
When you run this app and make requests, you'll see log entries like:
INFO:__main__:Request started: GET http://localhost:8000/slow
INFO:__main__:Request completed: GET http://localhost:8000/slow - Status: 200 - Time: 1.0032s
Order of Middleware Execution
The order in which you add middleware matters. Middleware is executed in the order they are added:
from fastapi import FastAPI, Request
app = FastAPI()
@app.middleware("http")
async def middleware1(request: Request, call_next):
print("Middleware 1 - Before")
response = await call_next(request)
print("Middleware 1 - After")
return response
@app.middleware("http")
async def middleware2(request: Request, call_next):
print("Middleware 2 - Before")
response = await call_next(request)
print("Middleware 2 - After")
return response
@app.get("/")
async def root():
print("Route handler executed")
return {"message": "Hello World"}
When you make a request to this app, the console will show:
Middleware 2 - Before
Middleware 1 - Before
Route handler executed
Middleware 1 - After
Middleware 2 - After
Notice how the middleware executes in a "nested" pattern, with the last middleware added being the first to process the request.
Summary
Middleware in FastAPI provides a powerful way to add functionality across your entire application without repeating code in every route handler. Key points to remember:
- Middleware runs before and after your route handlers
- Middleware can modify both requests and responses
- Multiple middleware are executed in the order they were added
- FastAPI provides built-in middleware for common tasks like CORS and compression
- Custom middleware can be created for authentication, logging, and other cross-cutting concerns
Exercises
- Create a middleware that adds a random request ID to each response header
- Implement a rate limiting middleware that restricts users to 10 requests per minute
- Build a middleware that checks if the API is in maintenance mode and returns a 503 response if it is
- Create a middleware that validates a JWT token for protected routes
Additional Resources
- FastAPI Official Documentation on Middleware
- ASGI Specification
- Starlette Middleware Documentation (FastAPI is built on top of Starlette)
- Python asyncio Documentation (for understanding async middleware)
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)