CodeOath
← All posts
Architecture & Patterns50 min total · 13 parts

Middleware Pipelines Compared: ASP.NET Core, Express.js, and Django

Part 4 of 13 · ~2 min

Prototype Two: Express.js

app.use((req, res, next) => {
  console.log(`${req.method} ${req.path}`);
  next(); // hand off to whatever's next — no await needed
});

app.use(authenticate);
app.post("/returns/:returnId/approve", approveReturn);

Same handoff, no await needed — calling next() is enough to move the request along. Skip that call, or skip sending a response some other way, and an Express request doesn't fail loudly. There's no exception, no automatic timeout kicking in. The connection just stays open, waiting on a reply that nothing left in the chain is ever going to send, until whatever's on the other end eventually gives up on its own.

Order Is the Whole API

// Runs for every route below this line
app.use(express.json());

// Runs only for this one route, layered in as extra arguments
app.post("/returns/:returnId/approve", authenticate, requireManager, approveReturn);

// Runs for every route under this one prefix
app.use("/admin", requireManager, adminDashboardRouter);

Express doesn't give branching a dedicated keyword the way ASP.NET Core's Map does — a path prefix handed to app.use and a stack of functions placed ahead of one route's handler are really the same registration-order trick, just aimed at two different scopes: one covers an entire URL prefix, the other covers a single route. Counter's regional dashboard picks up its manager check exactly the same way the approval route does — one more entry in the list, this one scoped to everything under /admin.

Error Handlers Are Just Functions With One Extra Argument

app.post("/returns/:returnId/approve", authenticate, requireManager, (req, res, next) => {
  approveReturn(req.params.returnId, req.body)
    .then((result) => res.json(result))
    .catch(next); // hands the rejection to Express's error-handling chain
});

// FOUR parameters — err, req, res, next — is the entire signal Express needs
app.use((err, req, res, next) => {
  console.error(err);
  res.status(err.status || 500).json({ error: "could not process the return" });
});

Express decides whether a middleware is an error handler purely by counting its parameters. Four instead of three flags it as special-cased, and it only fires once something further up the chain hands next a value — either through an explicit next(err), or through a plain throw inside a synchronous handler. The moment that happens, Express stops offering the request to any ordinary three-argument function still queued up and scans forward until it lands on one built to take four. That scan is the entire error-routing mechanism — there's nothing more elaborate underneath it.

That .catch(next) on approveReturn(...) is doing real work, not decoration — and exactly how much work depends on which major version of Express is actually running underneath it, a wrinkle worth flagging now and coming back to properly once the error-handling chapter gets there.