Express.js Middleware Patterns: Building Robust APIs
Express.js middleware is the backbone of robust Node.js applications. Understanding middleware patterns will help you build scalable, maintainable APIs that handle real-world requirements elegantly.
Understanding Middleware Flow
Middleware functions execute sequentially and have access to the request object (req), response object (res), and the next middleware function in the application’s request-response cycle.
const express = require('express');
const app = express();
// Basic middleware structure
app.use((req, res, next) => {
console.log('Time:', Date.now());
next(); // Pass control to next middleware
});
// Route-specific middleware
app.get('/protected', authenticate, authorize, (req, res) => {
res.json({ message: 'Access granted' });
});
Authentication Middleware
JWT Authentication
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Access denied. No token provided.' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(400).json({ error: 'Invalid token.' });
}
};
// Usage
app.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});
Role-Based Authorization
const authorize = (roles = []) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (roles.length && !roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
};
// Usage
app.delete('/admin/users/:id',
authenticate,
authorize(['admin']),
deleteUser
);
Validation Middleware
Request Validation with Joi
const Joi = require('joi');
const validate = (schema) => {
return (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({
error: 'Validation failed',
details: error.details.map(detail => detail.message)
});
}
next();
};
};
// Define schemas
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required()
});
// Usage
app.post('/users', validate(userSchema), createUser);
Custom Validation Middleware
const validateUser = (req, res, next) => {
const { name, email, password } = req.body;
const errors = [];
if (!name || name.length < 2) {
errors.push('Name must be at least 2 characters long');
}
if (!email || !/\S+@\S+\.\S+/.test(email)) {
errors.push('Valid email is required');
}
if (!password || password.length < 8) {
errors.push('Password must be at least 8 characters long');
}
if (errors.length > 0) {
return res.status(400).json({ errors });
}
next();
};
Error Handling Middleware
Global Error Handler
const errorHandler = (err, req, res, next) => {
console.error(err.stack);
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({
error: 'Validation Error',
details: errors
});
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Invalid token' });
}
// MongoDB duplicate key error
if (err.code === 11000) {
return res.status(400).json({
error: 'Duplicate field value',
field: Object.keys(err.keyValue)[0]
});
}
// Default error
res.status(err.status || 500).json({
error: err.message || 'Internal Server Error'
});
};
// Use at the end of middleware stack
app.use(errorHandler);
Async Error Wrapper
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage with async route handlers
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.find();
res.json(users);
}));
Rate Limiting Middleware
Basic Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
Custom Rate Limiting with Redis
const redis = require('redis');
const client = redis.createClient();
const customRateLimit = (maxRequests = 100, windowMs = 15 * 60 * 1000) => {
return async (req, res, next) => {
const key = `rate_limit:${req.ip}`;
try {
const current = await client.incr(key);
if (current === 1) {
await client.expire(key, Math.ceil(windowMs / 1000));
}
if (current > maxRequests) {
return res.status(429).json({
error: 'Rate limit exceeded',
retryAfter: await client.ttl(key)
});
}
res.set({
'X-RateLimit-Limit': maxRequests,
'X-RateLimit-Remaining': Math.max(0, maxRequests - current),
'X-RateLimit-Reset': new Date(Date.now() + windowMs)
});
next();
} catch (error) {
console.error('Rate limiting error:', error);
next(); // Fail open
}
};
};
Logging and Monitoring Middleware
Request Logging
const morgan = require('morgan');
// Custom log format
morgan.token('user', (req) => {
return req.user ? req.user.id : 'anonymous';
});
const logFormat = ':method :url :status :res[content-length] - :response-time ms - User: :user';
app.use(morgan(logFormat));
Performance Monitoring
const performanceMiddleware = (req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const end = process.hrtime.bigint();
const duration = Number(end - start) / 1000000; // Convert to milliseconds
console.log(`${req.method} ${req.url} - ${duration.toFixed(2)}ms`);
// Log slow requests
if (duration > 1000) {
console.warn(`Slow request detected: ${req.method} ${req.url} - ${duration.toFixed(2)}ms`);
}
});
next();
};
app.use(performanceMiddleware);
Security Middleware
CORS Configuration
const cors = require('cors');
const corsOptions = {
origin: (origin, callback) => {
const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(',') || [];
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
optionsSuccessStatus: 200
};
app.use(cors(corsOptions));
Security Headers
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
Middleware Composition
Combining Middleware
const apiMiddleware = [
express.json({ limit: '10mb' }),
express.urlencoded({ extended: true }),
cors(corsOptions),
helmet(),
morgan('combined'),
performanceMiddleware
];
app.use('/api', ...apiMiddleware);
// Route-specific middleware composition
const protectedRoute = [authenticate, authorize(['user', 'admin'])];
app.get('/api/protected', ...protectedRoute, handler);
Middleware patterns are the foundation of maintainable Express applications. By composing small, focused middleware functions, you create flexible and testable code that can evolve with your application’s needs.