Understanding Express.js Middleware
Middleware functions are the backbone of Express.js applications. They have access to the request object (req), response object (res), and the next middleware function in the application's request-response cycle.
This comprehensive guide covers everything from basic middleware concepts to production-ready patterns used by companies at scale.
Middleware Fundamentals
Basic Middleware Structure
// Simple middleware function
function myMiddleware(req, res, next) {
// Do something with req/res
console.log('Request received:', req.method, req.url);
// Pass control to next middleware
next();
}
// Use middleware
app.use(myMiddleware);
Types of Middleware
- Application-level middleware: Bound to app instance
- Router-level middleware: Bound to router instance
- Error-handling middleware: Has 4 parameters (err, req, res, next)
- Built-in middleware: express.json(), express.static()
- Third-party middleware: helmet, cors, morgan
Essential Production Middleware
1. Security Middleware
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
// Helmet - Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", 'data:', 'https:']
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// CORS configuration
const corsOptions = {
origin: function (origin, callback) {
const whitelist = process.env.ALLOWED_ORIGINS.split(',');
if (whitelist.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: 'Too many requests from this IP',
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter rate limit for auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5,
skipSuccessfulRequests: true
});
app.use('/api/auth/', authLimiter);
2. Request Parsing Middleware
const express = require('express');
const compression = require('compression');
// Body parsers
app.use(express.json({
limit: '10mb',
verify: (req, res, buf) => {
req.rawBody = buf;
}
}));
app.use(express.urlencoded({
extended: true,
limit: '10mb'
}));
// Compression
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6
}));
3. Logging Middleware
const morgan = require('morgan');
const winston = require('winston');
const fs = require('fs');
const path = require('path');
// Winston logger setup
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Morgan HTTP request logging
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'logs', 'access.log'),
{ flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));
// Custom request logger
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('user-agent')
});
});
next();
});
4. Authentication Middleware
const jwt = require('jsonwebtoken');
// JWT authentication
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'No token provided' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid token' });
}
req.user = user;
next();
});
}
// Optional authentication
function optionalAuth(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (token) {
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (!err) {
req.user = user;
}
});
}
next();
}
// Role-based authorization
function requireRole(...roles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Usage
app.get('/api/protected', authenticateToken, (req, res) => {
res.json({ data: 'Protected data', user: req.user });
});
app.get('/api/admin', authenticateToken, requireRole('admin'), (req, res) => {
res.json({ data: 'Admin data' });
});
5. Validation Middleware
const { body, param, query, validationResult } = require('express-validator');
// Validation middleware factory
function validate(validations) {
return async (req, res, next) => {
await Promise.all(validations.map(validation => validation.run(req)));
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
errors: errors.array().map(err => ({
field: err.param,
message: err.msg
}))
});
}
next();
};
}
// Example validations
const userValidation = validate([
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must contain uppercase, lowercase, and number'),
body('age').optional().isInt({ min: 18, max: 120 })
]);
const idValidation = validate([
param('id').isMongoId()
]);
// Usage
app.post('/api/users', userValidation, createUser);
app.get('/api/users/:id', idValidation, getUser);
6. Error Handling Middleware
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Async wrapper to catch errors
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// 404 handler
app.use((req, res, next) => {
next(new AppError(`Cannot find ${req.originalUrl}`, 404));
});
// Global error handler
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Log error
logger.error({
message: err.message,
stack: err.stack,
url: req.url,
method: req.method
});
// Development error response
if (process.env.NODE_ENV === 'development') {
return res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
}
// Production error response
if (err.isOperational) {
return res.status(err.statusCode).json({
status: err.status,
message: err.message
});
}
// Programming or unknown errors
return res.status(500).json({
status: 'error',
message: 'Something went wrong'
});
});
// Usage
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json({ user });
}));
Advanced Middleware Patterns
1. Caching Middleware
const redis = require('redis');
const client = redis.createClient();
function cache(duration) {
return async (req, res, next) => {
if (req.method !== 'GET') {
return next();
}
const key = `cache:${req.originalUrl}`;
try {
const cachedResponse = await client.get(key);
if (cachedResponse) {
return res.json(JSON.parse(cachedResponse));
}
// Override res.json to cache response
const originalJson = res.json.bind(res);
res.json = (body) => {
client.setex(key, duration, JSON.stringify(body));
return originalJson(body);
};
next();
} catch (error) {
next(error);
}
};
}
// Usage
app.get('/api/products', cache(300), getProducts);
2. Request Context Middleware
const { v4: uuidv4 } = require('uuid');
const cls = require('cls-hooked');
const namespace = cls.createNamespace('request-context');
function requestContext(req, res, next) {
namespace.run(() => {
const requestId = uuidv4();
namespace.set('requestId', requestId);
namespace.set('userId', req.user?.id);
res.setHeader('X-Request-ID', requestId);
next();
});
}
// Helper to get context anywhere
function getRequestContext() {
return {
requestId: namespace.get('requestId'),
userId: namespace.get('userId')
};
}
app.use(requestContext);
3. API Versioning Middleware
function apiVersion(version) {
return (req, res, next) => {
req.apiVersion = version;
next();
};
}
// V1 routes
const v1Router = express.Router();
v1Router.use(apiVersion('v1'));
v1Router.get('/users', getUsersV1);
// V2 routes
const v2Router = express.Router();
v2Router.use(apiVersion('v2'));
v2Router.get('/users', getUsersV2);
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
4. Performance Monitoring
const prometheus = require('prom-client');
// Metrics
const httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code']
});
const httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
function metricsMiddleware(req, res, next) {
const start = process.hrtime();
res.on('finish', () => {
const duration = process.hrtime(start);
const durationSeconds = duration[0] + duration[1] / 1e9;
const route = req.route?.path || req.path;
httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(durationSeconds);
httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
}
app.use(metricsMiddleware);
// Metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', prometheus.register.contentType);
res.end(await prometheus.register.metrics());
});
Middleware Execution Order
const express = require('express');
const app = express();
// 1. Security (first)
app.use(helmet());
app.use(cors(corsOptions));
app.use(limiter);
// 2. Request parsing
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(compression());
// 3. Logging and monitoring
app.use(morgan('combined'));
app.use(metricsMiddleware);
app.use(requestContext);
// 4. Static files
app.use(express.static('public'));
// 5. Authentication (if needed globally)
app.use(optionalAuth);
// 6. Routes
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// 7. 404 handler
app.use(notFoundHandler);
// 8. Error handler (last)
app.use(errorHandler);
Testing Middleware
const request = require('supertest');
const express = require('express');
describe('Authentication Middleware', () => {
let app;
beforeEach(() => {
app = express();
app.use(express.json());
app.get('/protected', authenticateToken, (req, res) => {
res.json({ user: req.user });
});
});
test('should reject request without token', async () => {
const response = await request(app)
.get('/protected');
expect(response.status).toBe(401);
});
test('should accept valid token', async () => {
const token = generateTestToken({ id: 1, email: '[email protected]' });
const response = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${token}`);
expect(response.status).toBe(200);
expect(response.body.user).toBeDefined();
});
});
Performance Best Practices
- Order matters: Put frequently used middleware first
- Avoid heavy computation: Keep middleware fast
- Use async/await properly: Prevent blocking
- Cache when possible: Reduce redundant work
- Monitor performance: Track middleware execution time
Production Checklist
- ✅ Security headers configured (helmet)
- ✅ Rate limiting implemented
- ✅ CORS properly configured
- ✅ Request validation in place
- ✅ Error handling comprehensive
- ✅ Logging configured
- ✅ Authentication/authorization working
- ✅ Monitoring and metrics enabled
- ✅ Compression enabled
- ✅ Tests written for middleware
Conclusion
Mastering Express.js middleware is essential for building production-ready Node.js applications. From security to performance monitoring, middleware provides the foundation for scalable, maintainable applications.
Start with the basics, add security layers, implement proper error handling, and monitor everything. Your production application will thank you.