DEV Community

Mitha
Mitha

Posted on

[quick-revisit]: Express Middleware

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:

  1. The req (request) object.
  2. The res (response) object.
  3. The next() function.
  4. The err object (only accessible to Error-handling Middleware)

Middleware can do the following:

  • Execute application code.
  • Manipulate the req and res objects. (e.g.: attaching req.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
omits next() 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" });
});
Enter fullscreen mode Exit fullscreen mode

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 });
});
Enter fullscreen mode Exit fullscreen mode

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
  }
});
Enter fullscreen mode Exit fullscreen mode

(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)