Skip to content

Writing Custom Express Middleware

While Express ships built-ins and npm has middleware for almost everything, real applications always end up writing a few custom middleware functions of their own. This lesson walks through several practical examples.

Anatomy of a Custom Middleware Function

A custom middleware function is just a regular JavaScript function following the (req, res, next) signature. What makes it useful is what it does with req, res, or the flow of control before calling next().

The cleanest custom middleware functions do one thing, logging, auth, timing, and are named descriptively so their purpose is obvious wherever they are used.

function requestTimer(req, res, next) {
  req.startTime = Date.now();
  res.on('finish', () => {
    const ms = Date.now() - req.startTime;
    console.log(`${req.method} ${req.url} - ${ms}ms`);
  });
  next();
}

app.use(requestTimer);

This middleware records a start time and logs the elapsed duration once the response finishes, without blocking the request.

A Factory Pattern for Configurable Middleware

function requireRole(role) {
  return (req, res, next) => {
    if (req.user?.role !== role) {
      return res.status(403).json({ error: 'Forbidden' });
    }
    next();
  };
}

app.get('/admin', requireRole('admin'), adminHandler);
  • A "middleware factory" is a function that returns a middleware function, letting you configure it (like requireRole('admin')).
  • This is exactly the pattern used by third-party middleware like cors(options) and rateLimit(options).
  • Custom middleware can be applied globally, per-router, or per-route just like built-in middleware.
  • Keep configuration (roles, limits, options) as arguments rather than hardcoded values.

Custom Middleware Patterns Cheatsheet

Reusable shapes for the custom middleware you will write most often.

Purpose Shape Where to Apply
Logging (req, res, next) => { ...; next(); } Application-level
Auth guard (req, res, next) => { if (!ok) return res.status(401)...; next(); } Route or router level
Configurable guard function requireRole(role) { return (req, res, next) => {...}; } Individual routes
Request augmentation req.customField = value; next(); Application or router level
Timing/metrics Listens on res.on('finish', ...) Application-level

Example: A Simple Auth Guard

A minimal authentication guard checks for a token and either attaches user data to req or short-circuits with a 401 response.

function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Missing token' });
  }

  try {
    req.user = verifyToken(token); // your own verification logic
    next();
  } catch {
    res.status(401).json({ error: 'Invalid token' });
  }
}

app.get('/profile', requireAuth, (req, res) => {
  res.json({ user: req.user });
});

Combining Multiple Custom Middleware

Route handlers accept as many middleware functions as you pass them, letting you compose small, testable pieces instead of one large handler.

app.post(
  '/orders',
  requireAuth,
  requireRole('customer'),
  validateOrderBody,
  createOrder
);

Each function focuses on one concern: authentication, authorization, validation, then the actual business logic.

Common Mistakes

  • Writing middleware that does too many unrelated things instead of composing several focused functions.
  • Forgetting that a middleware factory must return a function, not run the logic immediately.
  • Mutating shared state instead of attaching data to the current req/res objects.
  • Not calling next() on every code path, including inside try/catch blocks.

Key Takeaways

  • Custom middleware is a plain (req, res, next) function you write for app-specific needs.
  • The middleware factory pattern lets you configure reusable middleware with arguments.
  • Compose several small middleware functions instead of one large handler.
  • Always call next() (or send a response) on every possible code path.

Pro Tip

Name custom middleware functions descriptively (requireAuth, logRequests, validateOrderBody) instead of anonymous arrow functions, stack traces and route definitions become far easier to read.