5 Express.js Middleware Patterns You'll Use in Every App
Middleware is Express's superpower. These patterns handle 80% of what you'll ever need.
Pattern 1: Request Logging (With Duration)
const morgan = require('morgan');
// Standard logging
app.use(morgan('combined'));
// Custom format with response time
app.use(morgan(':method :url :status :response-time ms - :res[content-length]', {
skip: (req, res) => res.statusCode < 400, // Only log errors
}));
// DIY version (no dependencies):
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
const status = res.statusCode;
const color = status >= 500 ? 'red' : status >= 400 ? 'yellow' : 'green';
console.log(`[${color}] ${req.method} ${req.path} ${status} ${duration}ms`);
});
next();
});
Pattern 2: Error Handling (Global Catch-All)
// Custom error class
class AppError extends Error {
constructor(message, statusCode, details = null) {
super(message);
this.statusCode = statusCode;
this.details = details;
this.isOperational = true;
}
}
// Async wrapper — eliminates try/catch in every route
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage in routes:
app.get('/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);
}));
// Global error handler — MUST be last middleware
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
// Don't leak stack traces in production
const response = {
error: {
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
...(err.details && { details: err.details }),
}
};
console.error(`[${statusCode}] ${err.message}`, err.stack);
res.status(statusCode).json(response);
});
Pattern 3: Authentication Middleware
const jwt = require('jsonwebtoken');
// Flexible auth middleware factory
function requireAuth(options = {}) {
const { required = true, role } = options;
return (req, res, next) => {
const header = req.headers.authorization;
if (!header?.startsWith('Bearer ')) {
if (required) return res.status(401).json({ error: 'Missing token' });
return next(); // Optional auth — continue without user
}
const token = header.slice(7);
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
req.user = payload; // Attach to request
// Role check
if (role && payload.role !== role) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
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('/profile', requireAuth(), (req, res) => {
res.json(req.user);
});
app.get('/admin', requireAuth({ role: 'admin' }), (req, res) => {
res.json({ secret: 'admin stuff' });
});
app.get('/feed', requireAuth({ required: false }), (req, res) => {
// req.user might be undefined — show personalized or public feed
res.json({ feed: req.user ? 'personalized' : 'public' });
});
Pattern 4: Request Validation
const { z } = require('zod');
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse({
body: req.body,
query: req.query,
params: req.params,
});
if (!result.success) {
const errors = result.error.flatten().fieldErrors;
return res.status(400).json({
error: 'Validation failed',
details: errors
});
}
// Replace request data with validated/transformed data
req.body = result.data.body;
req.query = result.data.query;
req.params = result.data.params;
next();
};
}
// Define schemas:
const createUserSchema = z.object({
body: z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
password: z.string().min(8),
role: z.enum(['user', 'admin']).default('user'),
}),
});
// Usage:
app.post('/users', validate(createUserSchema), async (req, res) => {
// req.body is already validated and typed
const user = await User.create(req.body);
res.status(201).json(user);
});
Pattern 5: Request ID Tracing
const crypto = require('crypto');
app.use((req, res, next) => {
// Accept existing request ID or generate new one
const requestId = req.headers['x-request-id']
|| crypto.randomUUID();
req.id = requestId;
res.setHeader('X-Request-ID', requestId);
// Include in all logs
const originalLog = console.log;
console.log = (...args) => {
originalLog(`[${requestId}]`, ...args);
};
next();
});
// Now every log line includes the request ID:
// [a1b2c3d4] GET /api/users 200 45ms
// [a1b2c3d4] Database query: SELECT * FROM users 12ms
// [e5f6g7h8] POST /api/orders 201 120ms
// Use with a logging library (pino):
const pino = require('pino');
const logger = pino();
app.use((req, res, next) => {
req.log = logger.child({ reqId: req.id });
req.log.info({ req: { method: req.method, url: req.url } }, 'incoming');
next();
});
Putting It All Together
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const app = express();
// Order matters!
app.use(helmet()); // Security headers
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json({ limit: '10kb' })); // Body parsing
app.use(requestId()); // Request tracing
app.use(morgan('combined')); // HTTP logging
// Routes
app.use('/api/auth', authRoutes); // No auth needed
app.use('/api/users', requireAuth(), userRoutes);
app.use('/api/admin', requireAuth({ role: 'admin' }), adminRoutes);
// Error handling (MUST be last)
app.use(notFoundHandler);
app.use(globalErrorHandler);
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on :${PORT}`);
});
Quick Reference
| Pattern | When to Use | Complexity |
|---|---|---|
| Request logging | Every app | Low |
| Error handling | Every app | Medium |
| Auth middleware | Protected routes | Medium |
| Validation | API endpoints | Low |
| Request ID | Debugging/production | Low |
Follow @armorbreak for more Node.js patterns and production-ready code.
Top comments (0)