Back to Blog
Node.jsMiddlewareExpressPatterns

Node.js Middleware Patterns

Master middleware patterns in Node.js. From Express to Koa to custom middleware chains.

B
Bootspring Team
Engineering
September 14, 2020
6 min read

Middleware is central to Node.js web frameworks. Here's how to write effective middleware.

Express Middleware Basics#

1const express = require('express'); 2const app = express(); 3 4// Basic middleware function 5function logger(req, res, next) { 6 console.log(`${req.method} ${req.path}`); 7 next(); // Pass control to next middleware 8} 9 10app.use(logger); 11 12// Middleware with error handling 13function errorHandler(err, req, res, next) { 14 console.error(err.stack); 15 res.status(500).json({ error: 'Something went wrong' }); 16} 17 18// Order matters 19app.use(express.json()); // Parse JSON first 20app.use(logger); // Then log 21app.use('/api', apiRouter); // Then routes 22app.use(errorHandler); // Errors last

Common Middleware Types#

1// Request logging 2function requestLogger(req, res, next) { 3 const start = Date.now(); 4 5 res.on('finish', () => { 6 const duration = Date.now() - start; 7 console.log(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`); 8 }); 9 10 next(); 11} 12 13// Authentication 14function authenticate(req, res, next) { 15 const token = req.headers.authorization?.split(' ')[1]; 16 17 if (!token) { 18 return res.status(401).json({ error: 'No token provided' }); 19 } 20 21 try { 22 const decoded = jwt.verify(token, process.env.JWT_SECRET); 23 req.user = decoded; 24 next(); 25 } catch (error) { 26 res.status(401).json({ error: 'Invalid token' }); 27 } 28} 29 30// Authorization 31function authorize(...roles) { 32 return (req, res, next) => { 33 if (!req.user) { 34 return res.status(401).json({ error: 'Not authenticated' }); 35 } 36 37 if (!roles.includes(req.user.role)) { 38 return res.status(403).json({ error: 'Not authorized' }); 39 } 40 41 next(); 42 }; 43} 44 45// Usage 46app.get('/admin', authenticate, authorize('admin'), (req, res) => { 47 res.json({ message: 'Admin area' }); 48}); 49 50// Validation 51function validateBody(schema) { 52 return (req, res, next) => { 53 const { error } = schema.validate(req.body); 54 55 if (error) { 56 return res.status(400).json({ 57 error: 'Validation failed', 58 details: error.details, 59 }); 60 } 61 62 next(); 63 }; 64}

Async Middleware#

1// Async wrapper for Express 2function asyncHandler(fn) { 3 return (req, res, next) => { 4 Promise.resolve(fn(req, res, next)).catch(next); 5 }; 6} 7 8// Usage 9app.get('/users/:id', asyncHandler(async (req, res) => { 10 const user = await User.findById(req.params.id); 11 12 if (!user) { 13 throw new NotFoundError('User not found'); 14 } 15 16 res.json(user); 17})); 18 19// Alternative: express-async-handler 20const asyncHandler = require('express-async-handler'); 21 22app.get('/posts', asyncHandler(async (req, res) => { 23 const posts = await Post.find(); 24 res.json(posts); 25}));

Error Handling Middleware#

1// Custom error classes 2class AppError extends Error { 3 constructor(message, statusCode) { 4 super(message); 5 this.statusCode = statusCode; 6 this.isOperational = true; 7 } 8} 9 10class NotFoundError extends AppError { 11 constructor(resource = 'Resource') { 12 super(`${resource} not found`, 404); 13 } 14} 15 16class ValidationError extends AppError { 17 constructor(errors) { 18 super('Validation failed', 400); 19 this.errors = errors; 20 } 21} 22 23// Error handling middleware 24function errorHandler(err, req, res, next) { 25 // Log error 26 console.error(err); 27 28 // Operational errors (expected) 29 if (err.isOperational) { 30 return res.status(err.statusCode).json({ 31 status: 'error', 32 message: err.message, 33 ...(err.errors && { errors: err.errors }), 34 }); 35 } 36 37 // Programming errors (unexpected) 38 res.status(500).json({ 39 status: 'error', 40 message: 'Internal server error', 41 }); 42} 43 44// 404 handler 45function notFoundHandler(req, res) { 46 res.status(404).json({ 47 status: 'error', 48 message: `Route ${req.path} not found`, 49 }); 50} 51 52// Order: routes, 404, errors 53app.use('/api', routes); 54app.use(notFoundHandler); 55app.use(errorHandler);

Rate Limiting#

1// Simple rate limiter 2function rateLimit(options = {}) { 3 const { windowMs = 60000, max = 100 } = options; 4 const requests = new Map(); 5 6 // Cleanup old entries 7 setInterval(() => { 8 const now = Date.now(); 9 for (const [key, data] of requests) { 10 if (now - data.windowStart > windowMs) { 11 requests.delete(key); 12 } 13 } 14 }, windowMs); 15 16 return (req, res, next) => { 17 const key = req.ip; 18 const now = Date.now(); 19 const data = requests.get(key) || { count: 0, windowStart: now }; 20 21 // Reset window if expired 22 if (now - data.windowStart > windowMs) { 23 data.count = 0; 24 data.windowStart = now; 25 } 26 27 data.count++; 28 requests.set(key, data); 29 30 // Set headers 31 res.setHeader('X-RateLimit-Limit', max); 32 res.setHeader('X-RateLimit-Remaining', Math.max(0, max - data.count)); 33 34 if (data.count > max) { 35 return res.status(429).json({ error: 'Too many requests' }); 36 } 37 38 next(); 39 }; 40} 41 42app.use('/api', rateLimit({ windowMs: 60000, max: 100 }));

Caching Middleware#

1// Simple response cache 2function cache(duration) { 3 const store = new Map(); 4 5 return (req, res, next) => { 6 // Only cache GET requests 7 if (req.method !== 'GET') { 8 return next(); 9 } 10 11 const key = req.originalUrl; 12 const cached = store.get(key); 13 14 if (cached && Date.now() < cached.expiry) { 15 return res.json(cached.data); 16 } 17 18 // Override res.json to cache response 19 const originalJson = res.json.bind(res); 20 res.json = (data) => { 21 store.set(key, { 22 data, 23 expiry: Date.now() + duration, 24 }); 25 return originalJson(data); 26 }; 27 28 next(); 29 }; 30} 31 32app.get('/api/products', cache(60000), asyncHandler(async (req, res) => { 33 const products = await Product.find(); 34 res.json(products); 35}));

Request Context#

1// Add request context 2function requestContext(req, res, next) { 3 req.context = { 4 requestId: crypto.randomUUID(), 5 startTime: Date.now(), 6 ip: req.ip, 7 userAgent: req.get('User-Agent'), 8 }; 9 10 // Add request ID to response 11 res.setHeader('X-Request-ID', req.context.requestId); 12 13 next(); 14} 15 16// Correlation ID for distributed tracing 17function correlationId(req, res, next) { 18 req.correlationId = 19 req.get('X-Correlation-ID') || crypto.randomUUID(); 20 21 res.setHeader('X-Correlation-ID', req.correlationId); 22 23 next(); 24}

Middleware Composition#

1// Compose multiple middlewares 2function compose(...middlewares) { 3 return (req, res, next) => { 4 let index = 0; 5 6 function dispatch(i) { 7 if (i >= middlewares.length) { 8 return next(); 9 } 10 11 const middleware = middlewares[i]; 12 middleware(req, res, (err) => { 13 if (err) return next(err); 14 dispatch(i + 1); 15 }); 16 } 17 18 dispatch(0); 19 }; 20} 21 22// Usage 23const apiMiddleware = compose( 24 requestContext, 25 authenticate, 26 rateLimit({ max: 100 }), 27); 28 29app.use('/api', apiMiddleware); 30 31// Conditional middleware 32function when(condition, middleware) { 33 return (req, res, next) => { 34 if (condition(req)) { 35 return middleware(req, res, next); 36 } 37 next(); 38 }; 39} 40 41// Only authenticate non-public routes 42app.use(when( 43 (req) => !req.path.startsWith('/public'), 44 authenticate 45));

Koa-style Middleware#

1// Koa uses async/await natively 2const Koa = require('koa'); 3const app = new Koa(); 4 5// Koa middleware 6app.use(async (ctx, next) => { 7 const start = Date.now(); 8 await next(); // Wait for downstream 9 const ms = Date.now() - start; 10 ctx.set('X-Response-Time', `${ms}ms`); 11}); 12 13// Error handling 14app.use(async (ctx, next) => { 15 try { 16 await next(); 17 } catch (err) { 18 ctx.status = err.status || 500; 19 ctx.body = { error: err.message }; 20 ctx.app.emit('error', err, ctx); 21 } 22}); 23 24// Compose Koa middleware 25const compose = require('koa-compose'); 26 27const stack = compose([ 28 logger, 29 errorHandler, 30 bodyParser, 31]); 32 33app.use(stack);

Testing Middleware#

1const request = require('supertest'); 2const express = require('express'); 3 4function createApp(middleware) { 5 const app = express(); 6 app.use(express.json()); 7 app.use(middleware); 8 app.get('/test', (req, res) => res.json({ success: true })); 9 return app; 10} 11 12describe('authenticate middleware', () => { 13 it('should reject without token', async () => { 14 const app = createApp(authenticate); 15 16 const response = await request(app) 17 .get('/test') 18 .expect(401); 19 20 expect(response.body.error).toBe('No token provided'); 21 }); 22 23 it('should accept valid token', async () => { 24 const app = createApp(authenticate); 25 const token = jwt.sign({ id: 1 }, process.env.JWT_SECRET); 26 27 await request(app) 28 .get('/test') 29 .set('Authorization', `Bearer ${token}`) 30 .expect(200); 31 }); 32});

Best Practices#

Design: ✓ Single responsibility ✓ Configurable via options ✓ Return middleware function ✓ Handle errors properly Order: ✓ Security first (helmet, cors) ✓ Parsing (json, urlencoded) ✓ Logging ✓ Authentication ✓ Routes ✓ 404 handler ✓ Error handler Performance: ✓ Early returns when possible ✓ Avoid blocking operations ✓ Use async properly ✓ Cache when appropriate Testing: ✓ Test in isolation ✓ Test error cases ✓ Test middleware order ✓ Mock dependencies

Conclusion#

Middleware is the backbone of Node.js web applications. Write focused, composable middleware functions. Handle errors properly with dedicated error middleware. Use async handlers for Promise-based code. Test middleware in isolation and consider the order carefully.

Share this article

Help spread the word about Bootspring