Course outline

Express.js / Basic: request and response ownership / Lesson 3

Middleware order and control flow

Build an ordered request pipeline where each branch has one clear owner of completion or error propagation.

About 4 min readContent reviewed
On this page

Lesson goals

  • Predict which middleware runs after next, response completion or next(error).
  • Place parsing, identity, domain checks and final handlers according to their dependencies.

Registration order is part of application behavior

Express middleware forms an ordered stack of matching handlers. A handler registered earlier can reject a request before later routes see it, attach data those routes need or accidentally consume a response intended for another handler. app.use can mount middleware at a path prefix, while a route method combines a method and route pattern. Ordering is therefore not cosmetic organization. Write the dependency chain explicitly: a body-dependent validator needs parsing first, an object policy needs an established principal, and an error formatter belongs after code that can forward errors.

Place boundary work before domain work

The fragment shows an API pipeline with bounded JSON parsing, injected identity middleware and a course router. A final ordinary handler owns unmatched requests, and an error handler owns forwarded failures. The names stand for separately reviewed dependencies rather than built-in Express authentication or validation. Body parsing before identity is a design choice, not a universal rule: an application may authenticate from headers earlier to reject unauthenticated work before parsing, while another needs a verified raw body for a signed callback. Choose order from the real protocol and resource constraints.

javascript Illustrative ordered pipeline
app.use("/api", express.json({limit:"16kb"}));
app.use("/api", establishPrincipal);
app.use("/api/courses", courseRouter);
app.use((req,res) => res.status(404).json({code:"ROUTE_NOT_FOUND"}));
app.use((err,req,res,next) => {
  if (res.headersSent) return next(err);
  return res.status(500).json({code:"INTERNAL_ERROR"});
});

The 16kb bound is a synthetic lab choice, not a universal production recommendation. Every named dependency and the express package remain unexecuted.

Treat next as a transfer, not a second completion path

Calling next() continues normal matching middleware. Calling next(error) transfers to error handling and skips ordinary handlers, except for the documented special control strings in applicable contexts. A handler that already sent its final response should not also call next merely as a routine cleanup action; later middleware could attempt another response. Likewise, calling next twice can create duplicate execution paths. Put cleanup in the correct lifecycle owner and ensure callbacks, promises and catch branches do not each independently forward the same failure. Control transfer deserves the same care as return values in ordinary functions.

Model early rejection without claiming Express execution

The local pipeline below runs simple synchronous functions over a synthetic state. Returning false stops the model, allowing the test to prove that a denied principal never reaches domain work. It intentionally omits route matching, asynchronous next behavior and Express error dispatch. Those omissions keep its evidence narrow and deterministic. The useful invariant is that the prerequisite gate precedes the protected effect. A real operation-level test should apply the same invariant to the actual middleware composition, including the rejection response and the absence of a write call.

javascript Complete offline prerequisite-order model
function run(steps,state) {
  for (const step of steps) if (step(state) === false) break;
  return state;
}
const state = {allowed:false, trace:[]};
run([
  s => {s.trace.push("identity");},
  s => {s.trace.push("policy"); return s.allowed;},
  s => {s.trace.push("write");}
],state);
console.assert(JSON.stringify(state.trace) === '["identity","policy"]');

The denied fixture records identity and policy only. This is an ordinary loop, not an implementation of Express next or error middleware.

Keep request state separate from process state

Attach per-request information to a documented request property or res.locals rather than a mutable process-global currentUser variable. Overlapping requests can otherwise overwrite one another’s identity. Application-wide configuration and immutable service dependencies have different lifetimes from principals, timing state and request-scoped caches. A router mounted at a prefix also changes how path-related request properties should be interpreted, so use the documented originalUrl, baseUrl and path semantics instead of concatenating strings by guesswork. Review both registration order and state lifetime when diagnosing a pipeline that behaves differently under concurrent requests.

Common errors and fixes

A validator sees no parsed body

Why it happens
The body-dependent validator ran before the matching parser.
How to fix it
Place the parser before that validator under the intended route scope.
Prevent it next time
Test the actual mounted stack with supported content types.

A later handler sends after rejection

Why it happens
The rejecting handler both ended the response and called next.
How to fix it
Use one clear completion path and return after the response.
Prevent it next time
Assert no downstream domain call after rejection.

One request uses another user’s identity

Why it happens
A mutable global stored request-specific state.
How to fix it
Move principal data into the request lifecycle.
Prevent it next time
Test overlapping synthetic principals without shared mutable identity.

Check your understanding

1. Should every API always parse JSON before authenticating?

Show worked solution

No. Order depends on the protocol. Header-based authentication may reject unauthenticated work before parsing, while some signed callbacks require a verified raw body and another identity procedure. Make dependencies and limits explicit, then test the selected sequence. Do not promote this lesson’s illustrative ordering into a universal security recipe.

2. What extra evidence is required beyond the local run loop?

Show worked solution

An Express integration needs to verify matching, async completion, the exact rejection response and whether later middleware or source calls ran. The loop proves only its own synchronous order and early-stop invariant. Preserve that distinction so a passing local assertion is not reported as a tested Express authentication pipeline.

Key takeaways

  • Order and completion ownership determine middleware behavior.
  • Request identity belongs to the request lifecycle.

Official references