The power of Express.js comes from the middleware pattern. Every HTTP request travels through a pipeline of functions that run in sequence. This article covers how middleware works, the three most critical categories (auth, logging, error handling) and how to write your own, with production-ready examples.
What Is Middleware?
Middleware is a function with the signature (req, res, next) => {}. Calling next() advances the pipeline to the next middleware; not calling it leaves the request hanging. next(err) skips ahead to the error handler.
// Simplest possible middleware
app.use((req, res, next) => {
req.startTime = Date.now();
next();
});
app.get('/', (req, res) => {
res.json({ took: Date.now() - req.startTime });
});
Request Logger
function requestLogger(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const dur = Date.now() - start;
const level = res.statusCode >= 500 ? 'ERROR' : res.statusCode >= 400 ? 'WARN' : 'INFO';
console.log(`[${level}] ${req.method} ${req.originalUrl} ${res.statusCode} ${dur}ms`);
});
next();
}
app.use(requestLogger);
Authentication Middleware
const jwt = require('jsonwebtoken');
async function requireAuth(req, res, next) {
const header = req.headers.authorization || '';
const token = header.startsWith('Bearer ') ? header.slice(7) : null;
if (!token) return res.status(401).json({ error: 'Token required' });
try {
const payload = jwt.verify(token, process.env.JWT_SECRET, {
algorithms: ['HS256']
});
const user = await db.users.findByPk(payload.sub);
if (!user || !user.active) {
return res.status(401).json({ error: 'Invalid user' });
}
req.user = user;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// Usage
app.get('/api/me', requireAuth, (req, res) => {
res.json(req.user);
});
Role-Based Authorization
function requireRole(...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: 'Insufficient permission' });
}
next();
};
}
// Chained use
app.delete('/api/users/:id', requireAuth, requireRole('admin'), async (req, res) => {
await db.users.destroy({ where: { id: req.params.id } });
res.status(204).end();
});
Error Handler
An Express error handler has the 4-argument signature (err, req, res, next) and must always be the last middleware in the pipeline.
class AppError extends Error {
constructor(status, message, code) {
super(message);
this.status = status;
this.code = code;
}
}
// Usage
if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
// Async route handler wrapper
function asyncHandler(fn) {
return (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
}
app.get('/api/users/:id', asyncHandler(async (req, res) => {
const user = await db.users.findByPk(req.params.id);
if (!user) throw new AppError(404, 'User not found', 'USER_NOT_FOUND');
res.json(user);
}));
// Global error handler
app.use((err, req, res, next) => {
const status = err.status || 500;
const reqId = req.id || Date.now().toString(36);
if (status >= 500) {
console.error(`[${reqId}] ${err.stack}`);
}
res.status(status).json({
error: err.message || 'Internal server error',
code: err.code,
requestId: reqId
});
});
Validation Middleware
const { z } = require('zod');
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
issues: result.error.flatten()
});
}
req.body = result.data; // parsed + trimmed
next();
};
}
const createUserSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2).max(100)
});
app.post('/api/users', validate(createUserSchema), async (req, res) => {
// req.body is validated and typed here
const user = await db.users.create(req.body);
res.status(201).json(user);
});
Rate Limit
const rateLimit = require('express-rate-limit');
// Global
app.use(rateLimit({ windowMs: 60000, max: 120 }));
// Sensitive endpoint
app.use('/login', rateLimit({
windowMs: 900000, max: 5,
message: { error: 'Too many login attempts' }
}));
Pipeline Order
Middleware order matters:
- Security (helmet, cors) — first
- Parser (express.json) — you cannot authenticate what you cannot read
- Logger — early so failed requests are logged too
- Rate limit — before authentication
- Authentication — before routes
- Routes
- 404 handler — after all routes
- Error handler — last
Conclusion
The middleware pattern is what makes Express both easy to learn and infinitely extensible. Keep each middleware focused on a single responsibility, composable and testable. With that discipline, even large Express apps stay tidy in three or four folders.
API design with Express/Fastify, auth architecture and production deploys Write to us