Middleware Patterns
Middleware is a fundamental concept in Node.js web development. It allows you to process requests through a chain of functions, each performing a specific task before passing control to the next. Understanding middleware is essential for building maintainable and modular web applications.
What is Middleware?
Middleware is a function that sits between the incoming request and the final route handler. It can:
- Execute code
- Modify request and response objects
- End the request-response cycle
- Call the next middleware in the chain
function middleware(req, res, next) {
// Do something with req or res
console.log('Middleware executed');
// Pass to next middleware
next();
}
The Middleware Chain
Middleware functions are executed in order, forming a pipeline:
Request → [Logger] → [Auth] → [Validator] → [Route Handler] → Response
Common Middleware Types
1. Logging Middleware
function logger(req, res, next) {
const start = Date.now();
const { method, url } = req;
// Log when response finishes
res.on('finish', () => {
const duration = Date.now() - start;
console.log(`${method} ${url} ${res.statusCode} - ${duration}ms`);
});
next();
}
2. Authentication Middleware
function authenticate(req, res, next) {
const token = req.headers['authorization'];
if (!token) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'No token provided' }));
return;
}
try {
const user = verifyToken(token);
req.user = user;
next();
} catch (err) {
res.statusCode = 401;
res.end(JSON.stringify({ error: 'Invalid token' }));
}
}
3. CORS Middleware
function cors(req, res, next) {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Handle preflight
if (req.method === 'OPTIONS') {
res.statusCode = 204;
res.end();
return;
}
next();
}
Error Handling Middleware
Error handling middleware catches errors from previous middleware:
// Regular middleware that might throw
function riskyOperation(req, res, next) {
try {
// Something that might fail
doSomethingRisky();
next();
} catch (err) {
next(err); // Pass error to error handler
}
}
// Error handling middleware (4 parameters)
function errorHandler(err, req, res, next) {
console.error('Error:', err.message);
res.statusCode = err.status || 500;
res.end(JSON.stringify({
error: {
message: err.message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
}));
}
Route-Specific Middleware
Apply middleware to specific routes:
// Only for /admin routes
app.use('/admin', requireAdmin);
// Only for specific route
app.get('/users/:id', validateId, getUser);
// Multiple middleware on one route
app.post('/orders',
authenticate,
validateOrder,
checkInventory,
createOrder
);
Creating Reusable Middleware
Middleware Factory Pattern
function rateLimit(options = {}) {
const { max = 100, windowMs = 60000 } = options;
const requests = new Map();
return function(req, res, next) {
const ip = req.ip;
const now = Date.now();
const windowStart = now - windowMs;
// Clean old entries
const hits = (requests.get(ip) || [])
.filter(time => time > windowStart);
if (hits.length >= max) {
res.statusCode = 429;
res.end(JSON.stringify({ error: 'Too many requests' }));
return;
}
hits.push(now);
requests.set(ip, hits);
next();
};
}
// Usage
app.use(rateLimit({ max: 10, windowMs: 60000 }));
Async Middleware
Handle async operations in middleware:
// Wrap async middleware to catch errors
function asyncHandler(fn) {
return function(req, res, next) {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage
app.use(asyncHandler(async (req, res, next) => {
const user = await db.findUser(req.userId);
req.user = user;
next();
}));
Middleware Order Matters
The order you add middleware affects behavior:
// This order works:
app.use(bodyParser); // 1. Parse body first
app.use(auth); // 2. Then authenticate
app.use(routes); // 3. Then handle routes
// This fails - auth runs before body is parsed:
app.use(auth); // 1. Auth tries to check body
app.use(bodyParser); // 2. Body parsed too late!
Conditional Middleware
Apply middleware based on conditions:
// Only run in production
if (process.env.NODE_ENV === 'production') {
app.use(securityHeaders);
app.use(compression);
}
// Skip middleware for certain paths
function unless(paths, middleware) {
return function(req, res, next) {
if (paths.includes(req.path)) {
return next();
}
return middleware(req, res, next);
};
}
// Don't require auth for /login and /register
app.use(unless(['/login', '/register'], authenticate));
Best Practices
- Keep middleware focused - Each middleware should do one thing
- Always call next() - Unless you're ending the request
- Handle errors properly - Pass errors to error handling middleware
- Mind the order - Put general middleware before specific ones
- Use async handlers - Wrap async middleware to catch errors
- Make middleware configurable - Use factory functions
- Document your middleware - Explain what it does and expects
Key Takeaways
- Middleware is a function with
(req, res, next)signature - Middleware forms a chain - each calls
next()to continue - Not calling
next()stops the chain (useful for auth failures) - Error handling middleware has 4 parameters:
(err, req, res, next) - Use factory functions to create configurable middleware
- Order matters - add middleware in the right sequence
- Wrap async middleware to catch Promise rejections
Summary
Middleware is a powerful pattern for organizing request processing in Node.js applications. You've learned how to create middleware chains, handle errors, build reusable middleware factories, and apply middleware conditionally. These patterns are fundamental to frameworks like Express and are essential for building maintainable APIs.
In the next module, you'll learn about error handling and best practices for building robust Node.js applications.

