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 lastCommon 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.