KAHIBARO
Discord Login Register

6.11. Middleware

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:

It is like a chain of small helpers that each can:

A useful mental model:

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:

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:


ConcernTypical implementation place
AuthenticationMiddleware
Authorization checksMiddleware or route logic
Request loggingMiddleware
Rate limitingMiddleware
CORS handlingMiddleware
Compression (gzip, etc.)Middleware or web server
Error handlingMiddleware
Content-type negotiationMiddleware
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:

  1. Request enters application
  2. Middleware A runs (before)
  3. Middleware B runs (before)
  4. Route handler runs and produces response
  5. Middleware B runs (after)
  6. Middleware A runs (after)
  7. Response is returned to the client

It is like nested Russian dolls:

text
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):

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

text
A before
B before
Handler
B after
A after

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

It does not change the behavior of the request significantly.

Example: simple logging middleware (pseudo Python):

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 response

2. Transforming Middleware

This type modifies the request or response, but still lets the request go through.

Examples:

Pseudo example, adding a header to every response:

python
def header_middleware(request, call_next):
    response = call_next(request)
    response.headers["X-App-Version"] = "1.0.0"
    return response

3. Short-Circuiting Middleware

This type can stop the request from going further down the chain. It might:

Example: authentication middleware that checks a token and rejects if invalid:

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

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

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

This will log lines like:

text
GET /api/tasks -> 200 in 3.45 ms
POST /api/tasks -> 201 in 7.93 ms

Authentication Middleware

Authentication is a classic middleware job. You can:

Simplified example:

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

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

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

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

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

json
{
  "success": true,
  "data": { ... },
  "error": null
}

Instead of returning this structure in every handler, you can write middleware that wraps plain results.

Pseudo example:

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

Global vs Route-Specific Middleware

Most frameworks support:

Global Middleware

Use global middleware for logic that must always run, such as:

This is usually added in application setup, for example:

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

Conceptually:

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

Middleware Ordering

The order in which you register middleware matters, because of the before / after wrapping pattern.

Imagine you register middleware in this order:

python
app.add_middleware(AuthMiddleware)
app.add_middleware(LoggingMiddleware)

Then for a request, the order of operations is typically:

  1. AuthMiddleware before
  2. LoggingMiddleware before
  3. Handler
  4. LoggingMiddleware after
  5. AuthMiddleware after

If you swap them:

python
app.add_middleware(LoggingMiddleware)
app.add_middleware(AuthMiddleware)

Then the order becomes:

  1. LoggingMiddleware before
  2. AuthMiddleware before
  3. Handler
  4. AuthMiddleware after
  5. LoggingMiddleware after

This can change behavior. For example:

Good patterns:

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:

How they relate:

When to Use Middleware

Use middleware when:

Examples: CORS, compression, general logging, error handling.

When to Use Decorators

Use decorators when:

Examples:

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

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:

For example, instead of a giant AppMiddleware that does logging, authentication, and rate limiting, split them:

This makes it easier to test, reuse, and reason about.

Be Careful with Side Effects

Middleware often has side effects:

If possible:

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:

Always check:

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:

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

python
def wrap_with_middleware(middleware, next_handler):
    def wrapped(request):
        # middleware receives request and the next handler
        return middleware(request, next_handler)
    return wrapped

And a middleware would look like:

python
def logging_middleware(request, next_handler):
    print(f"Before {request.path}")
    response = next_handler(request)
    print(f"After {request.path}")
    return response

This is not production code, but it shows the basic principle of how middleware composes.


Summary

You have learned that:

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

Comments

Please login to add a comment.

Don't have an account? Register now!