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