6.11. Middleware
Table of Contents
Understanding Middleware in Web Backends
Middleware is one of those words you see everywhere in backend development, but it often feels vague. In this chapter you will learn what middleware is, why it exists, where it fits in the request pipeline, and see lots of concrete examples.
You will not learn framework specific APIs here, that belongs in later framework chapters. Instead, you will learn the general concept that applies to most backends, for example FastAPI, Express, Django, Laravel, and others.
What Is Middleware?
In a web backend, a request comes in from the client and a response goes back to the client.
Middleware is code that runs in between:
- between the web server and your route handler, or
- between one part of your application and another.
It is like a chain of small helpers that each can:
- look at the incoming request
- optionally modify the request
- decide whether to continue or stop the request
- optionally run some code after the response is created
- optionally modify the response before it is sent back
A useful mental model:
- The client sends an HTTP request.
- The request enters a pipeline of middleware functions.
- Each middleware can:
- do something before your main handler
- call the next part of the pipeline
- do something after the next part finishes
- At the end of the pipeline, your route handler creates a response.
- The response travels back up the chain, and each middleware can modify it.
You can think of middleware as filters or layers around your actual handlers.
Why Middleware Exists
Without middleware, you would need to copy the same code into many route handlers. For example, you might want to:
- log every request
- check authentication for protected routes
- measure how long a request took
- add common security headers to every response
- parse JSON request bodies
- handle errors in a consistent way
Instead of repeating this in every endpoint, you write one middleware and register it globally or for specific routes.
Key idea: Middleware is for cross cutting concerns, behavior that should apply to many or all routes in a consistent way.
Examples of cross cutting concerns:
| Concern | Typical implementation place |
|---|---|
| Authentication | Middleware |
| Authorization checks | Middleware or route logic |
| Request logging | Middleware |
| Rate limiting | Middleware |
| CORS handling | Middleware |
| Compression (gzip, etc.) | Middleware or web server |
| Error handling | Middleware |
| Content-type negotiation | Middleware |
| Body parsing (JSON, forms) | Middleware |
Request Pipeline and Middleware Chain
To understand middleware, it helps to imagine the lifecycle of a request.
A Simple Pipeline
A simplified pipeline:
- Request enters application
- Middleware A runs (before)
- Middleware B runs (before)
- Route handler runs and produces response
- Middleware B runs (after)
- Middleware A runs (after)
- Response is returned to the client
It is like nested Russian dolls:
Client
-> [Middleware A: before]
-> [Middleware B: before]
-> [Route handler]
<- [Middleware B: after]
<- [Middleware A: after]
<- Response to client
Each middleware wraps around the next one.
Conceptually in pseudocode (Python style):
def middleware_a(request, call_next):
print("A before")
response = call_next(request)
print("A after")
return response
def middleware_b(request, call_next):
print("B before")
response = call_next(request)
print("B after")
return response
def handler(request):
print("Handler")
return "OK"If A wraps B, and B wraps handler, the console output will be:
A before
B before
Handler
B after
A afterThis before / after pattern is the essence of many middleware systems.
Types of Middleware Behavior
Most middleware implementations can be grouped into a few patterns.
1. Passive (Observing) Middleware
This type only reads the request and/or response, for example:
- logging the method and path
- measuring duration
- collecting metrics
It does not change the behavior of the request significantly.
Example: simple logging middleware (pseudo Python):
def logging_middleware(request, call_next):
print(f"Incoming {request.method} {request.path}")
response = call_next(request)
print(f"Outgoing response status {response.status_code}")
return response2. Transforming Middleware
This type modifies the request or response, but still lets the request go through.
Examples:
- Parsing JSON and attaching
request.jsondata. - Adding or removing headers.
- Compressing the response body.
- Adding a standard response structure.
Pseudo example, adding a header to every response:
def header_middleware(request, call_next):
response = call_next(request)
response.headers["X-App-Version"] = "1.0.0"
return response3. Short-Circuiting Middleware
This type can stop the request from going further down the chain. It might:
- reject unauthorized requests
- block invalid data
- enforce rate limits
- return errors early
Example: authentication middleware that checks a token and rejects if invalid:
def auth_middleware(request, call_next):
token = request.headers.get("Authorization")
if token != "secret-token":
# Stop here, do not call next
return Response(status_code=401, body="Unauthorized")
# Token is ok, continue
return call_next(request)
The route handler will only be reached if the middleware calls call_next.
4. Exception Handling Middleware
This type wraps the handler, catches any exceptions, and returns a proper response, instead of crashing the application.
def error_middleware(request, call_next):
try:
return call_next(request)
except Exception as exc:
# log the error
print(f"Unhandled error: {exc}")
# return a generic error response
return Response(status_code=500, body="Internal Server Error")Most frameworks include some version of this by default.
Common Use Cases with Concrete Examples
Now let us look at typical things you would implement with middleware, with conceptual examples.
Logging Requests and Responses
You usually want to see what is happening in your backend.
Example: log method, path, status, and time taken.
import time
def logging_middleware(request, call_next):
start = time.time()
response = call_next(request)
duration_ms = (time.time() - start) * 1000
print(
f"{request.method} {request.path} "
f"-> {response.status_code} in {duration_ms:.2f} ms"
)
return responseThis will log lines like:
GET /api/tasks -> 200 in 3.45 ms
POST /api/tasks -> 201 in 7.93 msAuthentication Middleware
Authentication is a classic middleware job. You can:
- read the
Authorizationheader - verify a token or session
- attach the user object to the request for later use
Simplified example:
def auth_middleware(request, call_next):
auth_header = request.headers.get("Authorization")
user = None
if auth_header and auth_header.startswith("Bearer "):
token = auth_header[7:]
user = get_user_from_token(token) # your function
request.user = user # attach to request
return call_next(request)Then inside a route handler:
def get_profile(request):
if request.user is None:
return Response(status_code=401, body="Unauthorized")
return Response(body={"username": request.user.username})
Here, the handler does not know about tokens, it just uses request.user.
CORS Middleware
CORS (Cross Origin Resource Sharing) headers decide which browsers are allowed to call your API from different domains. Instead of adding headers in every route, you use middleware.
Pseudo example:
ALLOWED_ORIGINS = {"https://myfrontend.com"}
def cors_middleware(request, call_next):
response = call_next(request)
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
return responseRate Limiting Middleware
To protect your API, you might want to limit how many requests a client can make in a period.
Simple example using a dictionary as in memory counter:
import time
REQUEST_COUNTS = {} # key: client IP, value: list of timestamps
WINDOW_SECONDS = 60
MAX_REQUESTS = 10
def rate_limit_middleware(request, call_next):
ip = request.client_ip
now = time.time()
timestamps = REQUEST_COUNTS.get(ip, [])
# keep only recent ones
timestamps = [t for t in timestamps if now - t < WINDOW_SECONDS]
if len(timestamps) >= MAX_REQUESTS:
return Response(status_code=429, body="Too Many Requests")
timestamps.append(now)
REQUEST_COUNTS[ip] = timestamps
return call_next(request)This is not production grade, but it shows the idea.
Response Wrapping Middleware
Sometimes you want all responses to follow the same format, for example:
{
"success": true,
"data": { ... },
"error": null
}Instead of returning this structure in every handler, you can write middleware that wraps plain results.
Pseudo example:
def response_wrapper_middleware(request, call_next):
response = call_next(request)
# If handler already returned proper structure, do nothing
if isinstance(response.body, dict) and "success" in response.body:
return response
wrapped = {
"success": 200 <= response.status_code < 300,
"data": response.body if response.status_code < 400 else None,
"error": None if response.status_code < 400 else response.body,
}
response.body = wrapped
return responseGlobal vs Route-Specific Middleware
Most frameworks support:
- Global middleware that runs for every request.
- Route specific middleware that runs only for certain paths or route groups.
Global Middleware
Use global middleware for logic that must always run, such as:
- security headers
- general logging
- exception handling
- compression
This is usually added in application setup, for example:
app.add_middleware(LoggingMiddleware)
app.add_middleware(ErrorMiddleware)(Exact API depends on the framework.)
Route-Specific Middleware
Sometimes you only need logic for specific routes. Examples:
- Admin routes only
- Payment related endpoints
- Webhook endpoints from a specific provider
Conceptually:
@router.get("/admin", middlewares=[admin_auth_middleware])
def admin_dashboard(request):
...Or by adding middleware to a router / blueprint / module that only covers some paths.
Good practice:
- Use global middleware for cross cutting concerns that apply to the entire application.
- Use route specific middleware when only some endpoints need that behavior.
Middleware Ordering
The order in which you register middleware matters, because of the before / after wrapping pattern.
Imagine you register middleware in this order:
app.add_middleware(AuthMiddleware)
app.add_middleware(LoggingMiddleware)Then for a request, the order of operations is typically:
AuthMiddlewarebeforeLoggingMiddlewarebefore- Handler
LoggingMiddlewareafterAuthMiddlewareafter
If you swap them:
app.add_middleware(LoggingMiddleware)
app.add_middleware(AuthMiddleware)Then the order becomes:
LoggingMiddlewarebeforeAuthMiddlewarebefore- Handler
AuthMiddlewareafterLoggingMiddlewareafter
This can change behavior. For example:
- If
LoggingMiddlewareis beforeAuthMiddleware, then logs might not know which user is authenticated. - If
ErrorMiddlewareis not the outermost layer, some errors might escape.
Good patterns:
- Put error handling middleware near the outside, so it can catch exceptions from inner layers.
- Put authentication before authorization related middlewares or logic that assumes a user exists.
- Put logging outermost if it needs to see the total effect of all middlewares.
Rule: Middleware order defines the order of execution. The first registered middleware usually wraps all later ones. Choose an order that makes sense for your cross cutting concerns.
Many framework docs show the exact rules for that framework. Always check, because details can differ.
Middleware vs Route Decorators vs Hooks
You will see different ways to implement cross cutting logic:
- Middleware: wraps all or many routes at the application or router level.
- Decorators on specific handlers, for example
@login_required. - Hooks like "before request" or "after request" functions.
How they relate:
When to Use Middleware
Use middleware when:
- You want behavior to apply broadly across many routes.
- You need access to the raw request and response objects.
- You want a single, central place to control something.
Examples: CORS, compression, general logging, error handling.
When to Use Decorators
Use decorators when:
- The behavior is about business logic on a small set of routes.
- You want to reuse code among several handlers, but not globally.
Examples:
@require_auth
def create_task(request):
...
@require_admin
def delete_user(request, user_id):
...
Internally, decorators can use information that middleware attached to the request, such as request.user.
When to Use Hooks
Some frameworks provide hooks like:
before_requestafter_requeston_startupon_shutdown
These are usually global events that are similar to very simple forms of middleware. For example, before_request is like a middleware that only has "before" behavior.
Practical Patterns and Tips
Keep Middleware Focused and Small
It is tempting to put many things into one middleware file. That can quickly become hard to maintain.
Better approach:
- One middleware should usually do one thing well.
- You can still group related behavior if it is simple.
For example, instead of a giant AppMiddleware that does logging, authentication, and rate limiting, split them:
LoggingMiddlewareAuthMiddlewareRateLimitMiddleware
This makes it easier to test, reuse, and reason about.
Be Careful with Side Effects
Middleware often has side effects:
- writing logs
- writing to a database
- calling other services
If possible:
- Keep side effects idempotent or careful about retries.
- Do not do very slow operations inside middleware if it affects all requests.
For heavy work (for example sending emails, large logging writes), consider background tasks. You will learn more about this in the Background Processing section.
Respect the Framework’s Conventions
Even though the concept is the same, frameworks have different:
- function signatures
- ways of registering middleware
- expectations about sync vs async code
Always check:
- Does the middleware function need to be async?
- How do I access the request and response?
- How do I stop the chain and return early?
Reading a few example middlewares in your chosen framework will make this clear.
Middleware in a Simple Web Server Example
To tie everything together, consider a minimal pseudo framework.
Imagine a simple server API:
class App:
def __init__(self):
self.middlewares = []
self.routes = {}
def add_middleware(self, middleware):
self.middlewares.append(middleware)
def add_route(self, path, handler):
self.routes[path] = handler
def handle_request(self, request):
# Build the chain
handler = self.routes.get(request.path, not_found_handler)
for mw in reversed(self.middlewares):
# Each middleware takes the next handler and returns a new handler
handler = wrap_with_middleware(mw, handler)
# Call the outermost handler
return handler(request)
Where wrap_with_middleware could look like:
def wrap_with_middleware(middleware, next_handler):
def wrapped(request):
# middleware receives request and the next handler
return middleware(request, next_handler)
return wrappedAnd a middleware would look like:
def logging_middleware(request, next_handler):
print(f"Before {request.path}")
response = next_handler(request)
print(f"After {request.path}")
return responseThis is not production code, but it shows the basic principle of how middleware composes.
Summary
You have learned that:
- Middleware is code that runs around your route handlers, both before and after.
- It is used for cross cutting concerns like logging, authentication, CORS, rate limiting, and error handling.
- Middleware can:
- observe requests
- transform requests or responses
- short circuit and return early
- handle exceptions
- Middleware can be global or route specific, and ordering matters.
- Middleware complements decorators and hooks, each with its own role.
Understanding middleware conceptually will make it much easier to use any specific framework’s implementation later, including FastAPI’s middleware system that you will meet in the FastAPI section.
Views: 8
KAHIBARO