The Mental Model
Think of Express Middleware as a stack of functions that are called sequentially. Express apps run entirely through a series of middleware function calls.
The Basics
Middleware has access to the following:
- The
req(request) object. - The
res(response) object. - The
next()function. - The
errobject (only accessible to Error-handling Middleware)
Middleware can do the following:
- Execute application code.
- Manipulate the
reqandresobjects. (e.g.: attachingreq.user) - End request-response lifecycle (e.g.
res.json()). - Pass control to the next middleware function using
next().
4 Rules That Prevent Silent Bugs
1. Every Middleware Must Pass the Torch (next() vs. res.send())
- Call
next()to yield control to the next function down the stack. - Send a response (e.g.,
res.json()) to close the socket connection.
⚠️ The Endless Spinner Gotcha: If your middleware executes logic but
omitsnext()without sending a response, the client browser will hang
until hit by a socket timeout.
2. Leverage Middleware Chaining (Separation of Concerns)
Instead of bundling authentication, role checks, and database queries inside a single route controller, chain lightweight single-purpose middlewares directly within the route definition:
// Middleware 1: Verify token & attach user
const authenticate = (req, res, next) => {
req.user = { id: 101, role: 'admin' }; // Upstream mutation
next();
};
// Middleware 2: Role-based access control (RBAC)
const requireAdmin = (req, res, next) => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ error: "Access denied" });
}
next();
};
// Chained Route Handler
app.post('/admin/settings', authenticate, requireAdmin, (req, res) => {
res.json({ status: "Settings updated" });
});
3. Watch the 4-Parameter Arity Trap (Error Handling)
Express uses JavaScript's Function.length property to inspect handler signatures. An error handler must explicitly declare all 4 parameters: (err, req, res, next).
// ❌ FAILS: Express treats this as standard app middleware and ignores thrown errors
app.use((err, req, res) => {
res.status(500).send("Error");
});
// ✅ WORKS: Express recognizes the 4-argument signature via arity
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
4. Catch Async Exceptions (Express 4 vs. Express 5)
In Express 4, thrown errors inside async functions are not automatically caught by the global error handler. You must explicitly pass caught promises to next(err) — or wrap your routes in an async handler utility:
// Express 4 Async Gotcha: Must call next(err) explicitly
app.get('/user/:id', async (req, res, next) => {
try {
const user = await User.findById(req.params.id);
res.json(user);
} catch (err) {
next(err); // Without this, unhandled rejections crash the process or hang
}
});
(Note: Express 5 natively handles rejected promises in async middleware routes automatically).
Quick Reference: The 5 Middleware Types
| Type | Syntax Scope | Primary Use Case |
|---|---|---|
| App-Level | app.use() |
Global logging, CORS policies, static assets. |
| Router-Level | router.use() |
Scoped API versions (e.g., locking /api/v1/*). |
| Chained | app.post('/path', m1, m2) |
Single-route validation, payload sanitization. |
| Error-Handling | app.use((err, req, res, next)) |
Global exception formatting and error logging. |
Top comments (0)