Authoring Middleware

From "Just A Function" to Configurable Factories

Authoring Middleware

Write your own middleware. Plain functions for one-off, factories for reusable.

4 min read Level 1/5 #express#middleware#patterns
What you'll learn
  • Author a basic middleware
  • Use the factory pattern for configurable middleware
  • Attach values to req

A middleware is just a function. Let’s go from trivial to production-ready.

A Simple Logger

function logger(req, res, next) {
  console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
  next();
}

app.use(logger);

Configurable — Factory Pattern

The same logger, but you can pick a log level:

function logger({ level = "info" } = {}) {
  return (req, res, next) => {
    console[level](`${new Date().toISOString()} ${req.method} ${req.url}`);
    next();
  };
}

app.use(logger({ level: "debug" }));

The outer function takes config and returns the actual middleware. Most third-party middleware uses this patternexpress.json(), cors(), helmet() are all factory calls.

Attaching to req

A common pattern: middleware fetches something and attaches it for later handlers:

async function loadUser(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) {
    req.user = null;
    return next();
  }
  try {
    req.user = await verifyToken(token);
  } catch {
    req.user = null;
  }
  next();
}

app.use(loadUser);

app.get("/me", (req, res) => {
  if (!req.user) return res.status(401).end();
  res.json(req.user);
});

Subsequent handlers read req.user — no re-fetching, no re-validating.

Capturing Response Behavior

For after-the-fact work, hook into res.on("finish"):

function timing(req, res, next) {
  const start = Date.now();
  res.on("finish", () => {
    console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
  });
  next();
}

finish fires when the response is fully sent. Perfect for metrics and logs.

Don’t Forget next()

The #1 middleware bug: forgetting to call next(). The request hangs forever (or until the client times out).

function broken(req, res) {
  console.log("hi");
  // forgot next()! request never reaches the route handler
}

If your middleware doesn’t respond, it must call next().

Async Middleware

In Express 5, async middleware works just like async routes — throw and the error middleware catches:

async function requireFreshSession(req, res, next) {
  const session = await db.sessions.findById(req.sessionId);
  if (!session || session.expiresAt < Date.now()) {
    throw new Error("session expired");
  }
  req.session = session;
  next();
}
Built-in Middleware →