Middleware runs between routing and the handler, or around the whole request pipeline. It receives the request, the response, and a next function. Calling next() transfers control to the next processing context. A middleware can stop the chain by returning a response without calling next().
- CORS
The request first crosses the outer CORS policy.
- Logging
Logging wraps the downstream request and response.
- Authentication
Authentication can reject the request before the handler runs.
- Handler and return
The handler produces a response. It returns through authentication, logging, and CORS in reverse order.
Middleware wraps the next operation. A rejected request can stop before reaching the handler.
How to read this model
A nested enclosure with an inner handler fixture. The object is a teaching analogy. Inspecting a part highlights its role; it does not execute or simulate the software. Cabinet trays are records, workpieces are data, and latches are checks. Unless an explanation states a quantity, compartments and sample workpieces do not denote data counts. Part dimensions are not a quantitative scale.
Middleware can run code before next() and after it returns. That creates an onion-shaped flow. Logging starts a timer before next() and records duration after it. CORS can check the origin on entry and set response headers on exit.
Good middleware candidates
- CORS checks and response headers.
- Authentication that verifies a token and adds identity to context.
- Rate limiting that can reject a request with
429 Too Many Requests. - Request logging, metrics, tracing, and compression.
- Body parsing and shared validation.
- Global error handling that maps thrown errors to one response shape.
The test is practical. Use middleware when behavior applies to every request or a route group and needs request or response access. A handler's unique business rule belongs in the handler or service.
Order changes behavior
CORS -> request logging -> authentication -> route handler -> error handling
Reject disallowed origins and rate-limit requests early. Authenticate before sensitive work. Put error handling where the framework can observe errors from downstream handlers. In frameworks where errors travel forward through the chain, register the global error handler last. Confirm the framework's error model before choosing an order.
Return one structured error shape from the global handler:
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "You do not have enough balance.",
"status": 400
}
}
The code is stable for programs, the message is readable for people, and the status matches the HTTP result. Log the original exception with the request ID from request context.