Middleware Concepts - Function Stack That Processes Requests¶
Table of Contents¶
- What Is Middleware - The Function Stack
- Request Flow : Middleware -> Handler -> Response
- Writing Middleware from Scratch
- Error Handling Middleware (4 Args)
- Common Middleware Patterns
- Security : Middleware Ordering Matters
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
reqandresobjects - 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:
- Security (auth, CORS, rate limiting)
- Request parsing (body, cookies, compression)
- Business logic (routing, handlers)
- 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