Skip to content

Middleware Concepts - Function Stack That Processes Requests

Table of Contents


What Is Middleware - The Function Stack

Every incoming request passes through a chain of functions before reaching your route handler. Each function can:

  • Modify the req and res objects
  • End the response early (auth failure, rate limit)
  • Call the next function in the stack
// every middleware is just: (req, res, next) => { ... }

Think of middleware as layers of an onion

  • the request travels inward through each layer , hits the core handler , then the response travels back outward through the same layers

Request Flow : Middleware -> Handler -> Response

Visualize the flow:

Request ->
  [Logger] -> [Auth] -> [Body Parser] -> [Router] -> [Handler]
                                                     <- [Response]
const http = require('node:http')

function createMiddlewareStack(middlewares, handler) {
  return (req, res) => {
    let idx = 0

    function next() {
      if (res.writableEnded) return // someone already sent response

      if (idx < middlewares.length) {
        const middleware = middlewares[idx++]
        middleware(req, res, next)
      } else {
        handler(req, res)
      }
    }

    next()
  }
}

// middlewares
function logger(req, res, next) {
  const start = Date.now()
  console.log(`=> ${req.method} ${req.url}`)
  // capture the original end to log response time
  const originalEnd = res.end.bind(res)
  res.end = (...args) => {
    console.log(`← ${req.method} ${req.url} - ${Date.now() - start}ms`)
    return originalEnd(...args)
  }
  next()
}

function auth(req, res, next) {
  if (req.headers['x-api-key'] !== 'supersecret') {
    res.writeHead(401)
    return res.end('{"error": "unauthorized"}')
  }
  next()
}

function bodyParser(req, res, next) {
  if (req.method === 'POST' || req.method === 'PUT') {
    const chunks = []
    req.on('data', c => chunks.push(c))
    req.on('end', () => {
      const raw = Buffer.concat(chunks).toString()
      try { req.body = JSON.parse(raw) }
      catch { req.body = raw }
      next()
    })
  } else {
    next()
  }
}

// handler
function handler(req, res) {
  res.writeHead(200, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({
    url: req.url,
    body: req.body || null,
    authenticated: true
  }))
}

// compose
const app = createMiddlewareStack([logger, auth, bodyParser], handler)
http.createServer(app).listen(3000)

Writing Middleware from Scratch

Build a minimal Express-like middleware system:

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

  use(mw) {
    this.middlewares.push(mw)
  }

  get(path, handler) {
    this.handlers.push({ method: 'GET', path, handler })
  }

  post(path, handler) {
    this.handlers.push({ method: 'POST', path, handler })
  }

  async handle(req, res) {
    let idx = 0

    const next = async () => {
      if (res.writableEnded) return

      if (idx < this.middlewares.length) {
        const mw = this.middlewares[idx++]
        await mw(req, res, next)
        return
      }

      // find matching route
      const urlObj = new URL(req.url, `http://${req.headers.host}`)
      for (const route of this.handlers) {
        if (route.method !== req.method) continue
        // basic match (expand with param parsing if needed)
        if (route.path === urlObj.pathname) {
          return route.handler(req, res)
        }
      }

      res.writeHead(404)
      res.end('Not found')
    }

    await next()
  }

  listen(port, cb) {
    const server = http.createServer((req, res) => this.handle(req, res))
    server.listen(port, cb)
  }
}

// usage
const app = new App()

app.use((req, res, next) => {
  req.timestamp = Date.now()
  next()
})

app.use((req, res, next) => {
  res.setHeader('X-Powered-By', 'duct-tape')
  next()
})

app.get('/', (req, res) => {
  res.end(`Hello - timestamp: ${req.timestamp}`)
})

app.listen(3000)

Error Handling Middleware (4 Args)

Express-style error middleware has 4 parameters - Node.js reads fn.length to detect it:

function errorHandler(err, req, res, next) {
  console.error('Unhandled error:', err.stack || err.message)

  res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' })
  res.end(JSON.stringify({
    error: err.message || 'Internal Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  }))
}

Wrapping async handlers to catch errors:

function asyncHandler(fn) {
  return (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next)
  }
}

// error-aware middleware stack
function createStack(middlewares) {
  return async (req, res) => {
    let idx = 0

    async function next(err) {
      if (res.writableEnded) return

      if (err) {
        // find error middleware (4-arg functions)
        while (idx < middlewares.length) {
          const mw = middlewares[idx++]
          if (mw.length === 4) {
            return mw(err, req, res, next)
          }
        }
        // no error handler found - crash
        console.error('Unhandled error:', err)
        res.writeHead(500)
        return res.end('Internal Server Error')
      }

      if (idx < middlewares.length) {
        const mw = middlewares[idx++]
        try {
          if (mw.length === 4) return next() // skip error middleware in normal flow
          await mw(req, res, next)
        } catch (e) {
          next(e)
        }
      }
    }

    await next()
  }
}

// usage
const middlewares = [
  (req, res, next) => { req.user = { id: 1 }; next() },
  asyncHandler(async (req, res) => {
    throw new Error('DB connection failed') // this gets caught
  }),
  errorHandler
]

http.createServer(createStack(middlewares)).listen(3000)

Common Middleware Patterns

Logging:

function requestLogger(options = {}) {
  return (req, res, next) => {
    const start = Date.now()
    res.on('finish', () => {
      const ms = Date.now() - start
      console.log(`${req.method} ${req.url} ${res.statusCode} ${ms}ms`)
    })
    next()
  }
}

CORS:

function cors(options = {}) {
  const origins = options.origins || ['*']
  return (req, res, next) => {
    const origin = req.headers.origin
    if (origins.includes('*') || origins.includes(origin)) {
      res.setHeader('Access-Control-Allow-Origin', origin || '*')
    }
    res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')

    if (req.method === 'OPTIONS') {
      res.writeHead(204)
      return res.end()
    }
    next()
  }
}

Rate Limiting (in-memory):

function rateLimit(options = {}) {
  const windowMs = options.windowMs || 60000
  const max = options.max || 100
  const hits = new Map()

  setInterval(() => hits.clear(), windowMs) // reset every window

  return (req, res, next) => {
    const ip = req.socket.remoteAddress
    const count = (hits.get(ip) || 0) + 1
    hits.set(ip, count)

    res.setHeader('X-RateLimit-Limit', max)
    res.setHeader('X-RateLimit-Remaining', Math.max(0, max - count))

    if (count > max) {
      res.writeHead(429)
      return res.end('{"error": "rate limit exceeded"}')
    }
    next()
  }
}

Security : Middleware Ordering Matters

Order determines whether a middleware even runs. Put things in the wrong order and you open security holes

WRONG ORDER - auth after body parse means unauthenticated users can waste your server's CPU parsing giant payloads:

app.use(bodyParser)     // parses entire request body
app.use(authMiddleware) // THEN checks auth

An attacker sends 100MB POST bodies with no auth header - your server memory goes bye bye

CORRECT ORDER:

app.use(authMiddleware)  // fails fast for unauthenticated
app.use(rateLimiter)     // fails fast for flooders
app.use(bodyParser)      // only parse for authenticated users

Golden rule of middleware ordering:

  1. Security (auth, CORS, rate limiting)
  2. Request parsing (body, cookies, compression)
  3. Business logic (routing, handlers)
  4. Error handler (always last)

Global middleware ordering checklist: * Security middleware FIRST - reject before processing * Parsers SECOND - only after identity is established * Routers THIRD - dispatch after preparation * Error handler LAST - catch everything that falls through


prerequisites

web_03_routing.md - Route matching , params extraction , middleware in routing context

next => web_05_sessions.md