Routing decides which code should handle a request. Middleware decides what happens to that request on the way there.
This is the core of Express. If you understand the middleware pipeline well, many other backend concepts start feeling less mysterious.
Beginners need to learn the visible flow. Professionals need to design that flow so auth, logging, validation, and error handling stay orderly.
A surprising number of Express bugs come from middleware order, hidden side effects, or route matching assumptions.
Middleware is easiest to understand as shared processing that sits between the raw request and the final handler. It can inspect the request, reject it, enrich it, log it, or pass it onward.
Once you see middleware as a pipeline, Express becomes much easier to reason about. You stop thinking in isolated route functions and start thinking in stages of processing.
Express runs middleware in the order you register it. That means the exact placement of body parsing, auth checks, route registration, logging, and error handlers changes behavior. This is one of the first places where backend code stops feeling like ordinary sequential JavaScript and starts feeling like request orchestration.
Many beginners discover this through bugs: a route cannot read the request body, a protected route runs before auth middleware, or an error handler never sees the thrown error because the control flow is wrong.
Express processes matching middleware and handlers in registration order. Application middleware can add request IDs, security headers, parsers, and logging. Routers group feature routes. Route-level middleware performs authentication, authorization, or validation before the final controller.
A middleware function must send a response, call next(), or pass an error to next(error). Calling neither leaves the request waiting. Calling next after a response may cause another handler to write again. Async middleware should forward rejected promises according to the Express version and project wrapper strategy.
Order matters. Parse JSON before validating the body, authenticate before authorization, and register the final not-found handler after all routes. Error middleware comes last and uses the four-argument signature. Keep middleware narrow so the request path remains explainable.
In a larger service, middleware can quietly become a maze. Professionals keep it understandable by naming middleware clearly, limiting hidden mutation of the request object, and documenting shared route stacks.
A good request pipeline is not only functional. It is explainable. If a teammate cannot describe why a request was rejected or transformed, the pipeline is already too opaque.
Create public and protected routers, mount authentication before protected handlers, and place the error middleware last. Test the same endpoint with no token, an invalid token, and a valid token.
Work through this as a controlled engineering exercise rather than a copy-and-paste demo. State the expected result before running anything, keep the input small enough to inspect, and record the important intermediate state. That makes the lesson explain not only what to type, but why the result is trustworthy.
Mounting validation before parsing JSON produces missing fields, while mounting authorization after the handler exposes protected behavior. Error middleware with the wrong four-argument signature is ignored by Express.
Verification must use evidence that matches the concept. Assert which middleware ran for each case, the returned status, and that the protected handler never runs for rejected requests. Repeat the check after deliberately introducing the failure, then after the fix. The contrast between those runs is the part that turns a definition into practical understanding.
Configure proxy trust correctly before using forwarded IP or protocol headers. An overly broad trust setting lets clients spoof network identity, while no trust behind a real proxy breaks secure-cookie and address behavior. Normalize request IDs at the trusted edge and replace untrusted client copies.
Use AsyncLocalStorage or an equivalent context mechanism carefully for request-scoped logging and tracing. Do not use mutable globals. Bound body sizes and parser work before expensive authentication or application logic, and apply route-specific limits when upload and JSON endpoints have different risks.
Measure middleware latency and failure rates. Avoid synchronous CPU-heavy work and repeated database lookups in global middleware. Cache safe identity metadata briefly when appropriate, but re-check sensitive account state and resource permission at the operation boundary.
This is a realistic example of how several pieces cooperate before the handler responds.
Request -> logger -> JSON parser -> auth middleware -> role check -> route handler -> response -> error handler if something fails
Adapt this focused example to a disposable local environment and inspect every result before expanding it.
router.post('/orders',
authenticate,
validate(createOrderSchema),
createOrder
);
app.use(errorHandler);
This sequence separates global transport concerns from feature rules.
app.set("trust proxy", 1);
app.use(requestId());
app.use(securityHeaders());
app.use(express.json({ limit: "100kb" }));
app.use(requestLogger());
app.use("/orders", ordersRouter);
app.use(notFoundHandler);
app.use(errorHandler);
Each stage has one observable responsibility.
router.post("/",
authenticate,
authorize("orders:create"),
validate(createOrderSchema),
createOrderController
);
Yes. That is one of its most important jobs for validation, authentication, rate limiting, and other gatekeeping behavior.
No. Middleware is best for cross-cutting request concerns. Core business logic usually belongs elsewhere.
Explore 500+ free tutorials across 20+ languages and frameworks.