How to Create Custom FastAPI Middleware Classes for Enhanced Request Handling

You can implement custom FastAPI middleware classes by inheriting from Starlette's BaseHTTPMiddleware or creating ASGI-compatible callables, then registering them via app.add_middleware() or the @app.middleware("http") decorator.

FastAPI inherits its middleware architecture from Starlette, building an ASGI middleware stack that wraps every incoming request and outgoing response. When you create custom FastAPI middleware classes, you tap into this stack to execute logic before and after your route handlers run. This guide demonstrates how to construct, register, and optimize middleware using patterns found in the tiangolo/fastapi source code.

Understanding the FastAPI Middleware Architecture

FastAPI constructs its middleware pipeline in FastAPI.build_middleware_stack located in fastapi/applications.py. The method assembles a nested structure where each middleware wraps the next layer:

  1. ServerErrorMiddleware – Catches unhandled server errors at the outermost layer
  2. User middlewares – Your custom FastAPI middleware classes execute here
  3. ExceptionMiddleware – Converts exceptions to HTTP responses
  4. AsyncExitStackMiddleware – Guarantees resource cleanup via fastapi/middleware/asyncexitstack.py

This ordering means middleware added first runs outermost, processing the request first and the response last.

How to Register Custom FastAPI Middleware Classes

FastAPI provides two primary mechanisms for registering middleware, both ultimately producing ASGI-compatible callables that receive (scope, receive, send).

Using app.add_middleware() for Class-Based Middleware

The most common pattern for reusable components, add_middleware stores the class and initialization arguments in self.user_middleware (defined in FastAPI.__init__ at lines 479-493 of fastapi/applications.py).

from fastapi import FastAPI
from starlette.middleware.base import BaseHTTPMiddleware

class LogRequestMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        print(f"Incoming {request.method} {request.url.path}")
        response = await call_next(request)
        print(f"Response status {response.status_code}")
        return response

app = FastAPI()
app.add_middleware(LogRequestMiddleware)

Using the @app.middleware Decorator for Function-Based Middleware

For simple, one-off logic, the decorator creates a middleware function that receives the Request object and a call_next callable. This pattern appears in docs_src/middleware/tutorial001_py39.py.

import time
from fastapi import FastAPI, Request

app = FastAPI()

@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.perf_counter()
    response = await call_next(request)
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response

Building Custom FastAPI Middleware Classes

When creating class-based middleware, you can choose between Starlette's convenience base class or raw ASGI compatibility for maximum control.

Extending BaseHTTPMiddleware

The BaseHTTPMiddleware pattern abstracts the ASGI protocol, allowing you to work with Request and Response objects directly. Implement the dispatch method to process requests and responses.

from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
from starlette.responses import Response
import logging

logger = logging.getLogger(__name__)

class AuthenticationMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        # Pre-processing: validate headers

        token = request.headers.get("Authorization")
        if not token:
            return Response("Unauthorized", status_code=401)
        
        response = await call_next(request)
        
        # Post-processing: add security headers

        response.headers["X-Content-Type-Options"] = "nosniff"
        return response

Implementing Raw ASGI Middleware

For advanced use cases like request body inspection or streaming modifications, implement the ASGI interface directly. This pattern appears in tests/test_custom_middleware_exception.py with the ContentSizeLimitMiddleware.

from fastapi import FastAPI, HTTPException, UploadFile, File, APIRouter

class ContentSizeLimitMiddleware:
    """ASGI middleware to enforce maximum request body size."""
    
    def __init__(self, app, max_content_size: int | None = None):
        self.app = app
        self.max_content_size = max_content_size

    def receive_wrapper(self, receive):
        received = 0

        async def inner():
            nonlocal received
            message = await receive()
            if message["type"] != "http.request":
                return message
            received += len(message.get("body", b""))
            if self.max_content_size and received > self.max_content_size:
                raise HTTPException(
                    status_code=422,
                    detail={
                        "name": "ContentSizeLimitExceeded",
                        "code": 999,
                        "message": "File limit exceeded"
                    }
                )
            return message

        return inner

    async def __call__(self, scope, receive, send):
        if scope["type"] != "http" or self.max_content_size is None:
            await self.app(scope, receive, send)
            return
        wrapper = self.receive_wrapper(receive)
        await self.app(scope, wrapper, send)

router = APIRouter()

@router.post("/upload")
def upload(file: UploadFile = File(...)):
    return {"filename": file.filename}

app = FastAPI()
app.include_router(router)
app.add_middleware(ContentSizeLimitMiddleware, max_content_size=2**8)

Managing Middleware Execution Order

The sequence in which you add custom FastAPI middleware classes determines their execution order. As implemented in fastapi/applications.py lines 4610-4625, FastAPI constructs the stack as:


ServerErrorMiddleware → [User Middlewares] → ExceptionMiddleware → AsyncExitStackMiddleware

Execution flow:

  • Middleware added first wraps the application outermost
  • Request processing flows inward (first added sees request first)
  • Response processing flows outward (first added sees response last)

# Order example

app.add_middleware(LoggingMiddleware)      # Runs first on request, last on response

app.add_middleware(AuthenticationMiddleware)  # Runs second on request, second-to-last on response

app.add_middleware(GZipMiddleware)         # Runs last on request, first on response

Summary

  • FastAPI middleware inherits from Starlette's ASGI architecture, with the stack built in fastapi/applications.py via build_middleware_stack.
  • Register custom FastAPI middleware classes using app.add_middleware() for reusable components or @app.middleware("http") for simple function-based logic.
  • Implement middleware by extending BaseHTTPMiddleware for convenience, or implement __call__(self, scope, receive, send) for raw ASGI control over request bodies and streaming.
  • Execution order follows the "onion" pattern: middleware added first runs outermost, processing requests first and responses last.
  • Built-in middleware like GZipMiddleware and AsyncExitStackMiddleware demonstrate production patterns for compression and resource cleanup.

Frequently Asked Questions

What is the difference between app.add_middleware() and @app.middleware()?

app.add_middleware() accepts a class (typically inheriting from BaseHTTPMiddleware or implementing ASGI protocols) and stores it in self.user_middleware for later instantiation within the middleware stack. This approach supports configuration via constructor arguments. The @app.middleware("http") decorator wraps a function that receives request and call_next, creating a lightweight middleware ideal for simple cross-cutting concerns like timing or header injection.

How do I access the request body inside custom FastAPI middleware?

Standard BaseHTTPMiddleware cannot easily access the request body because consuming the stream prevents downstream handlers from reading it. To inspect body content, implement a raw ASGI middleware class with a receive_wrapper pattern that intercepts http.request messages while cloning the body for the original receiver. The ContentSizeLimitMiddleware in tests/test_custom_middleware_exception.py demonstrates this technique by wrapping the receive callable to monitor byte counts without breaking the ASGI flow.

Can I raise HTTPException from within custom middleware?

Yes, but only if your middleware is positioned correctly in the stack or implements proper exception handling. The ExceptionMiddleware sits after user middlewares in fastapi/applications.py, so raising HTTPException in a standard BaseHTTPMiddleware subclass works because Starlette's exception middleware catches it. However, in raw ASGI middleware, you must ensure exceptions propagate correctly to the ExceptionMiddleware or handle them within your __call__ method to prevent ASGI server errors.

What is the correct order for adding multiple middleware classes?

Middleware executes in the order added, following an "onion" model where the first middleware added becomes the outermost layer. In fastapi/applications.py, build_middleware_stack constructs the sequence as ServerErrorMiddleware → user middlewares (in addition order) → ExceptionMiddleware → AsyncExitStackMiddleware. Therefore, add security and logging middleware first, followed by transformation middleware like GZip compression last, ensuring compression occurs after authentication checks but before the response leaves the application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →