Anatomy of Custom Middleware
A middleware function is just a function with three arguments (or four for error handlers):
function myMiddleware(req, res, next) {
// 1. Read/modify req or res
// 2. End the response (send/end/json) OR
// 3. Call next() to pass control forward
}
Three Possible Outcomes
function sample(req, res, next) {
// Option A: Pass to next middleware
next();
// Option B: End the response here
res.status(400).json({ error: 'Bad request' });
// Option C: Pass an error
next(new Error('Something went wrong'));
}
Basic Logger Middleware
// Simple logger
function logger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ` +
`${res.statusCode} ${duration}ms`
);
});
next();
}
app.use(logger);
Authentication Middleware
// Token-based auth middleware
function authenticate(req, res, next) {
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({
error: 'Unauthorized',
message: 'No authorization header provided'
});
}
const token = authHeader.startsWith('Bearer ')
? authHeader.slice(7)
: authHeader;
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'Token expired',
message: 'Please refresh your token'
});
}
return res.status(403).json({
error: 'Invalid token',
message: 'Authentication failed'
});
}
}
// Apply to protected routes
app.get('/api/profile', authenticate, (req, res) => {
res.json({ user: req.user, message: 'Welcome to your profile' });
});
Authorization Middleware (Role-Based)
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: 'Forbidden',
message: `Requires one of: ${allowedRoles.join(', ')}`
});
}
next();
};
}
// Usage
app.get('/api/admin', authenticate, authorize('admin'), (req, res) => {
res.json({ secret: 'admin data' });
});
app.get('/api/moderator', authenticate, authorize('admin', 'moderator'), (req, res) => {
res.json({ secret: 'moderator data' });
});
Request Validation Middleware
function validate(schema) {
return (req, res, next) => {
const errors = [];
for (const [field, rules] of Object.entries(schema)) {
const value = req.body[field];
for (const rule of rules) {
if (rule.required && (value === undefined || value === null || value === '')) {
errors.push(`${field} is required`);
break;
}
if (value !== undefined && value !== null && value !== '') {
if (rule.type === 'string' && typeof value !== 'string') {
errors.push(`${field} must be a string`);
}
if (rule.type === 'number' && isNaN(Number(value))) {
errors.push(`${field} must be a number`);
}
if (rule.min !== undefined && value.length < rule.min) {
errors.push(`${field} must be at least ${rule.min} characters`);
}
if (rule.max !== undefined && value.length > rule.max) {
errors.push(`${field} must be at most ${rule.max} characters`);
}
if (rule.pattern && !rule.pattern.test(value)) {
errors.push(`${field} format is invalid`);
}
if (rule.custom) {
const customErr = rule.custom(value, req.body);
if (customErr) errors.push(customErr);
}
}
}
}
if (errors.length > 0) {
return res.status(422).json({ error: 'Validation failed', details: errors });
}
next();
};
}
// Schema definition
const userSchema = {
name: [
{ required: true },
{ type: 'string' },
{ min: 2 },
{ max: 50 }
],
email: [
{ required: true },
{ type: 'string' },
{ pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/ }
],
age: [
{ type: 'number' },
{ custom: (v) => v < 18 ? 'Must be 18 or older' : null }
]
};
// Usage
app.post('/api/users', validate(userSchema), (req, res) => {
res.status(201).json({ message: 'User created', data: req.body });
});
Request Timing Middleware
function requestTimer(label = 'Request') {
return (req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e6;
console.log(`${label}: ${req.method} ${req.originalUrl} took ${duration.toFixed(2)}ms`);
// Store for observability
if (!req.app.locals.timings) req.app.locals.timings = [];
req.app.locals.timings.push({
path: req.originalUrl,
method: req.method,
duration,
timestamp: new Date().toISOString()
});
});
next();
};
}
app.use(requestTimer('API'));
IP Whitelist Middleware
function ipWhitelist(allowedIPs) {
return (req, res, next) => {
const clientIP = req.ip || req.connection.remoteAddress;
if (!allowedIPs.includes(clientIP)) {
return res.status(403).json({
error: 'Forbidden',
message: 'Your IP is not allowed'
});
}
next();
};
}
// Only allow internal network
const internalIPs = ['127.0.0.1', '::1', '10.0.0.1', '192.168.1.100'];
app.use('/api/admin', ipWhitelist(internalIPs));
Configurable Middleware Factory
Create middleware factories (functions that return middleware) for reusability:
function pagination(defaultPage = 1, defaultLimit = 10) {
return (req, res, next) => {
const page = Math.max(1, parseInt(req.query.page) || defaultPage);
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || defaultLimit));
const skip = (page - 1) * limit;
req.pagination = { page, limit, skip };
next();
};
}
app.get('/api/users', pagination(), (req, res) => {
const { page, limit, skip } = req.pagination;
const data = allUsers.slice(skip, skip + limit);
res.json({
data,
pagination: {
page,
limit,
total: allUsers.length,
totalPages: Math.ceil(allUsers.length / limit)
}
});
});
Async Middleware Wrapper
Express 4 doesn’t catch rejected promises. Use a wrapper:
function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Instead of try/catch in every route
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) {
return res.status(404).json({ error: 'Not found' });
}
res.json(user);
}));
Header Manipulation Middleware
function securityHeaders(req, res, next) {
res.set({
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'X-XSS-Protection': '1; mode=block',
'Cache-Control': 'no-store',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains'
});
next();
}
function rateLimitHeaders(limit, remaining, reset) {
return (req, res, next) => {
res.set({
'X-RateLimit-Limit': limit,
'X-RateLimit-Remaining': remaining,
'X-RateLimit-Reset': reset
});
next();
};
}
Conditional Middleware
Run middleware only under certain conditions:
function ifMethod(methods, middleware) {
return (req, res, next) => {
if (methods.includes(req.method)) {
return middleware(req, res, next);
}
next();
};
}
// Only log POST requests
app.use(ifMethod(['POST', 'PUT', 'PATCH'], (req, res, next) => {
console.log('Mutation request:', req.method, req.path);
next();
}));
Middleware Execution Order
// Custom middleware that verifies execution order
function trace(name) {
return (req, res, next) => {
if (!req.trace) req.trace = [];
req.trace.push(name);
console.log(`[${name}] Entering`);
next();
console.log(`[${name}] Exiting`);
};
}
app.use(trace('A'));
app.use('/api', trace('B'));
app.get('/api/test', trace('C'), (req, res) => {
console.log('Trace:', req.trace);
res.json({ trace: req.trace });
});
// Console output:
// [A] Entering
// [B] Entering
// [C] Entering
// Trace: ['A', 'B', 'C']
// [C] Exiting
// [B] Exiting
// [A] Exiting
Best Practices
// ✅ Good: Configurable factory function
function myMiddleware(options = {}) {
return (req, res, next) => {
// use options
next();
};
}
// ✅ Good: Attach data to req for downstream middleware
function attachUser(req, res, next) {
req.user = { id: 1, name: 'Alice' };
next();
}
// ✅ Good: Handle errors with next()
function riskyOperation(req, res, next) {
try {
const result = doSomething();
req.result = result;
next();
} catch (err) {
next(err);
}
}
// ❌ Bad: Mutating res without calling next or ending
function badMiddleware(req, res, next) {
res.set('X-Custom', 'value');
// Forgot next() - request hangs
}
// ❌ Bad: Throwing synchronously
function badMiddleware(req, res, next) {
throw new Error('Will crash the app'); // Use next(new Error(...)) instead
}
Key Takeaways
- Middleware is just a function with
(req, res, next)signature - Use factory functions to make configurable, reusable middleware
- Attach data to
reqto pass between middleware (e.g.,req.user) - Always call
next()or end the response - never do both or neither - Wrap async middleware to handle rejected promises
- Use middleware for cross-cutting concerns: auth, logging, validation, timing
- Test middleware in isolation by mocking
req,res, andnext