Design Patterns, Explained Simply · Modern Patterns and Choosing Well

Middleware and Pipelines

Process a request through a chain of small, reusable steps (logging, auth, rate limiting) where each step can act before and after the next one, or stop the request.

The problem

Every API endpoint starts with the same lines: log the request, check the auth token, check the rate limit, parse JSON, catch errors and format them. Forty endpoints repeat them with small differences, and one forgot the auth check.

You want these cross-cutting steps written once, applied consistently, and easy to reorder or skip per route.

Real-world analogy

Airport security. Every passenger passes through a sequence of checkpoints: ticket check, ID check, bag scan, body scan. Each checkpoint can stop you, and each does one job. The gate (the handler) only sees passengers who passed every check, and new checkpoints can be added without redesigning the gate.

Without the pattern

def get_orders(request):
    log(request)
    if not valid_token(request.headers.get("Authorization")):
        return 401
    if rate_limited(request.ip):
        return 429
    ...                                    # the actual endpoint, after copy-pasted checks

The pattern

  • Handler: the final step that does the real work (handler(request) -> response).
  • Middleware: a function or object that receives the request and a next function. It can:
    • do work before calling next(request) (log, authenticate),
    • stop the request by returning a response without calling next (401, 429),
    • do work after next returns (add headers, measure time).
  • Pipeline: composes the middleware list around the handler, so each one wraps the rest.

This combines Chain of Responsibility (each step can stop the request) with Decorator (each step wraps the next with before/after behaviour).

The code

Composing logging, auth and a handler into a pipelineTry it yourself
def logging_mw(request, next_):
    print(f"-> {request['path']}")
    response = next_(request)  # before ... after
    print(f"<- {response['status']}")
    return response


def auth_mw(request, next_):
    if request.get("token") != "secret":
        return {"status": 401, "body": "unauthorized"}  # stop here
    request["user"] = "ada"
    return next_(request)


def handler(request):
    return {"status": 200, "body": f"orders for {request['user']}"}


def pipeline(middlewares, final):
    def build(index):
        if index == len(middlewares):
            return final
        return lambda req: middlewares[index](req, build(index + 1))
    return build(0)


app = pipeline([logging_mw, auth_mw], handler)
print(app({"path": "/orders", "token": "secret"})["body"])
print(app({"path": "/orders", "token": "wrong"})["body"])

Output

-> /orders
<- 200
orders for ada
-> /orders
<- 401
unauthorized

The handler never checks auth, and logging wraps both outcomes. A rate limiter would be one more function in the list.

When to use it

  • Requests or messages need the same cross-cutting steps: logging, auth, CORS, rate limiting, error handling, compression, transactions, tracing.
  • Steps should be reusable and reorderable, and different routes or queues need different combinations.
  • You process messages or jobs through stages (a queue consumer pipeline, a data-processing pipeline).

When not to use it

  • Logic that belongs to one endpoint. Middleware is for things that apply to many.
  • Middleware that depends on each other's side effects in hidden ways ("auth must run after session, which must run after cookies"). Document the order or make dependencies explicit.
  • Too many layers make a request's path hard to trace. Keep each middleware small and well named.

You've already used it

  • Express / Koa / Fastify (app.use((req, res, next) => ...)), Laravel middleware (handle($request, Closure $next)), Django middleware, ASP.NET Core pipeline, Starlette/FastAPI middleware.
  • Redux middleware (store => next => action => ...).
  • Laravel's Pipeline class and Java servlet filters.
  • Message pipelines: Celery and Sidekiq middleware, and gRPC interceptors.

Key points

  • Middleware wraps a handler with before/after behaviour and can stop a request.
  • A pipeline composes middleware so each wraps the next, combining Chain of Responsibility and Decorator.
  • Use it for cross-cutting concerns that apply to many routes or messages.
  • Further reading: Laravel middleware and Express: using middleware.

Practice questions

How does middleware combine Chain of Responsibility and Decorator?

Answer

Like Chain of Responsibility, each middleware can handle the request itself and stop the chain (return a 401). Like Decorator, each middleware wraps the next with behaviour before and after it (logging, timing, adding headers).

Why does the order of middleware matter?

Answer

Each middleware wraps the ones after it. Error handling placed first catches errors from everything inside; logging placed before auth logs rejected requests too; a body parser must run before code that reads the body.

What kinds of logic belong in middleware?

Answer

Cross-cutting concerns applied to many routes or messages: logging, tracing, authentication, CORS, rate limiting, error formatting, compression and transactions. Endpoint-specific business logic does not.

Open in the app to mark lessons complete, take notes and track your progress.