5 Express.js Middleware Patterns You'll Use in Every App
Middleware is Express's superpower. Here are the patterns I use in every project.
What is Middleware?
Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response
↓ ↓
(auth check) (logging)
// Basic structure
function middleware(req, res, next) {
// Do something with request/response
if (/* condition */) {
return next(); // Pass to next middleware/handler
}
res.status(403).json({ error: 'Forbidden' }); // Or end here
}
Pattern 1: Authentication Middleware
// Simple API key auth
function apiKeyAuth(req, res, next) {
const key = req.headers['x-api-key'];
if (!key) {
return res.status(401).json({ error: 'API key required' });
}
const user = validateApiKey(key); // Your validation logic
if (!user) {
return res.status(401).json({ error: 'Invalid API key' });
}
req.user = user; // Attach to request for downstream use
next();
}
// JWT auth (more common for web apps)
function jwtAuth(req, res, next) {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Missing token' });
}
const token = header.split(' ')[1];
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
return res.status(401).json({ error: 'Invalid token' });
}
}
// Usage
app.get('/api/profile', jwtAuth, (req, res) => {
res.json({ user: req.user }); // req.user is available!
});
Pattern 2: Error Handling Middleware
// Custom error class
class AppError extends Error {
constructor(message, statusCode = 500, code = 'INTERNAL_ERROR') {
super(message);
this.statusCode = statusCode;
this.code = code;
}
}
// Async handler wrapper (catches errors automatically)
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Routes using asyncHandler
app.get('/users', asyncHandler(async (req, res) => {
const users = await User.findAll(); // If this throws → caught by error middleware
res.json(users);
}));
// Global error handler (MUST be last middleware)
app.use((err, req, res, _next) => {
console.error('Error:', err);
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: err.message,
code: err.code,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
}
// Handle known errors
if (err.name === 'ValidationError') {
return res.status(422).json({ error: err.message, details: err.details });
}
if (err.code === 'SQLITE_CONSTRAINT') {
return res.status(409).json({ error: 'Resource already exists' });
}
// Unknown errors — don't leak details in production
const statusCode = err.statusCode || 500;
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
res.status(statusCode).json({ error: message });
});
Pattern 3: Request Validation
// Using express-validator (or zod, joi)
const { body, param, query, validationResult } = require('express-validator');
// Validation rules
const createUserRules = [
body('name')
.trim()
.isLength({ min: 2, max: 50 })
.withMessage('Name must be 2-50 characters'),
body('email')
.isEmail()
.normalizeEmail()
.withMessage('Must be a valid email'),
body('password')
.isLength({ min: 8 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.withMessage('Password must have uppercase, lowercase, and number'),
];
// Validation middleware
const validate = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: 'Validation failed',
details: errors.array().map(e => ({ field: e.path, message: e.msg })),
});
}
next();
};
// Usage
app.post(
'/api/users',
createUserRules, // Define rules
validate, // Check results
asyncHandler(async (req, res) => {
// Data is validated and clean!
const user = await User.create(req.body);
res.status(201).json(user);
}),
);
Pattern 4: Request Logging & Timing
// Development logger (verbose)
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
const start = Date.now();
// Log request details
console.log(`📥 ${req.method} ${req.originalUrl}`);
// Log when response finishes
res.on('finish', () => {
const duration = Date.now() - start;
const statusColor = res.statusCode < 400 ? '\x1b[32m' : '\x1b[31m';
console.log(
`${statusColor}${res.statusCode}\x1b[0m ` +
`${req.method} ${req.originalUrl} - ${duration}ms`
);
});
next();
});
}
// Production logger (structured JSON)
const morgan = require('morgan');
app.use(morgan('combined', {
stream: {
write: (message) => {
// Send to your logging service (Datadog, CloudWatch, etc.)
logToService(message.trim());
},
},
}));
Pattern 5: Rate Limiting + Security Headers
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');
// Security headers (always include!)
app.use(helmet());
// Rate limiting per endpoint
const strictLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
standardHeaders: true,
});
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 10,
skipSuccessfulRequests: true,
});
// Apply to routes
app.use('/api/', strictLimiter);
app.post('/api/login', authLimiter, loginHandler);
app.post('/api/register', authLimiter, registerHandler);
// CORS configuration
const cors = require('cors');
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
Bonus: Compose Middleware
// Combine multiple middleware into one
function compose(...middlewares) {
return (req, res, next) => {
let index = 0;
function run(i) {
if (i >= middlewares.length) return next();
middlewares[i](req, res, (err) => {
if (err) return next(err);
run(i + 1);
});
}
run(0);
};
}
// Usage
const authenticatedRoute = compose(
cors(),
rateLimit({ windowMs: 60000, max: 30 }),
jwtAuth,
attachPermissions,
);
app.get('/api/dashboard', authenticatedRoute, dashboardHandler);
Quick Reference
| Middleware | Purpose | Package |
|---|---|---|
helmet() |
Security headers | helmet |
cors() |
Cross-origin requests | cors |
rateLimit() |
Throttle requests | express-rate-limit |
morgan() |
HTTP logging | morgan |
express.json() |
Parse JSON bodies | built-in |
cookieParser() |
Parse cookies | cookie-parser |
Custom asyncHandler
|
Catch async errors | custom |
Custom validate
|
Input validation | express-validator/zod |
What's your go-to Express middleware pattern?
Follow @armorbreak for more Node.js content.
Top comments (0)