Skip to content
⚠️ This article was written in 2019. Some content may be outdated.

Node.js Middleware Pattern Explained

The middleware pattern is a fundamental design pattern in Node.js, best exemplified by Express.js. It allows you to compose request/response processing through a chain of functions. This article explores its core principles and practical applications.

What is Middleware?

Middleware functions have access to the request object (req), the response object (res), and the next function in the request-response cycle:

javascript
function middleware(req, res, next) {
  // Do something with req/res
  next(); // Pass control to the next middleware
}

Building a Simple Middleware System

javascript
class App {
  constructor() {
    this.middlewares = [];
  }

  use(fn) {
    this.middlewares.push(fn);
    return this;
  }

  run(req, res) {
    const dispatch = (index) => {
      if (index >= this.middlewares.length) return;
      const fn = this.middlewares[index];
      fn(req, res, () => dispatch(index + 1));
    };
    dispatch(0);
  }
}

const app = new App();

app.use((req, res, next) => {
  console.log("Logging:", req.method, req.url);
  next();
});

app.use((req, res, next) => {
  req.startTime = Date.now();
  next();
  // Note: code after next() runs after all downstream middleware
});

app.use((req, res) => {
  res.end("Hello World");
});

Koa-style Async Middleware

Koa uses an "onion model" — middleware can await next() and resume execution after all downstream middleware completes:

javascript
function compose(middlewares) {
  return function (ctx) {
    let index = -1;

    function dispatch(i) {
      if (i <= index)
        return Promise.reject(new Error("next() called multiple times"));
      index = i;

      if (i >= middlewares.length) return Promise.resolve();

      const fn = middlewares[i];
      return Promise.resolve(fn(ctx, () => dispatch(i + 1)));
    }

    return dispatch(0);
  };
}

// Usage
const middlewares = [
  async (ctx, next) => {
    console.log("middleware 1 start");
    await next();
    console.log("middleware 1 end");
  },
  async (ctx, next) => {
    console.log("middleware 2 start");
    await next();
    console.log("middleware 2 end");
  },
  async (ctx) => {
    console.log("middleware 3 (final)");
  },
];

// Output:
// middleware 1 start
// middleware 2 start
// middleware 3 (final)
// middleware 2 end
// middleware 1 end

Common Middleware Patterns

Logging Middleware

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

Authentication Middleware

javascript
function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) {
    return res.status(401).json({ error: "Unauthorized" });
  }
  try {
    req.user = verifyToken(token);
    next();
  } catch {
    res.status(401).json({ error: "Invalid token" });
  }
}

Error Handling Middleware

javascript
// Express error middleware takes 4 arguments
function errorHandler(err, req, res, next) {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: process.env.NODE_ENV === "production" ? "Server Error" : err.message,
  });
}

app.use(errorHandler); // Must be registered last

Rate Limiting Middleware

javascript
function rateLimit(maxRequests, windowMs) {
  const requests = new Map();

  return (req, res, next) => {
    const ip = req.ip;
    const now = Date.now();
    const windowStart = now - windowMs;

    // Clean up old entries
    if (requests.has(ip)) {
      const times = requests.get(ip).filter((t) => t > windowStart);
      requests.set(ip, times);
    }

    const times = requests.get(ip) || [];
    if (times.length >= maxRequests) {
      return res.status(429).json({ error: "Too Many Requests" });
    }

    times.push(now);
    requests.set(ip, times);
    next();
  };
}

app.use(rateLimit(100, 60 * 1000)); // 100 requests per minute

Summary

  • Middleware is a function chain where each function can process request/response and optionally call next()
  • Express uses a linear model; Koa uses the onion model (code before/after next() both run)
  • Common patterns: logging, authentication, error handling, rate limiting
  • The order of middleware registration matters — they execute in registration order

MIT Licensed