Skip to main content

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

Middleware Flow

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:

python
@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:

  1. @app.middleware("http") - A decorator that registers a function as middleware
  2. request - The incoming request object
  3. call_next - A function that passes the request to the next middleware or route handler
  4. response - 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:

python
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:

python
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:

python
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:

python
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:

  1. Send a request without an API key:

    curl http://localhost:8000/protected

    You'll get: {"detail":"Invalid or missing API key"}

  2. 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:

python
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:

python
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

  1. Create a middleware that adds a random request ID to each response header
  2. Implement a rate limiting middleware that restricts users to 10 requests per minute
  3. Build a middleware that checks if the API is in maintenance mode and returns a 503 response if it is
  4. Create a middleware that validates a JWT token for protected routes

Additional Resources



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)