How to Write Custom Middleware in Express
Writing custom middleware is a learnable skill. Here is how to write useful middlewares.
How to Write Custom Middleware in Express
Most Express apps need a few custom middlewares. Here is how to write them well.
The Basic Pattern
const myMiddleware = (req, res, next) => {
// do something with req or res
next(); // or send a response
};
When to Write Custom Middleware
- Auth (verify JWT, set req.user).
- Validation (check body, params, query).
- Logging (custom logger with context).
- Rate limiting (custom logic per route).
- Permission checks (role or ownership).
- Request ID (assign a unique ID for tracing).
Pattern 1: Auth Middleware
const auth = (req, res, next) => {
const token = req.cookies.token;
if (!token) return res.status(401).json({ error: 'Not authenticated' });
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ error: 'Invalid token' });
}
};
Pattern 2: Validation Middleware
Use a factory that takes a schema and returns a middleware:
const validate = (schema) => (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) return res.status(400).json({ errors: result.error.issues });
req.body = result.data;
next();
};
router.post('/signup', validate(signupSchema), signup);
Pattern 3: Request ID Middleware
const crypto = require('crypto');
const requestId = (req, res, next) => {
req.id = crypto.randomUUID();
res.setHeader('X-Request-Id', req.id);
next();
};
Useful for tracing logs across services.
Pattern 4: Async Middleware Wrapper
const asyncMiddleware = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
Wrap any async middleware so promise rejections go to the error handler.
Pattern 5: Permission Middleware
const requireRole = (role) => (req, res, next) => {
if (req.user?.role !== role) return res.status(403).json({ error: 'Not authorized' });
next();
};
router.use('/admin', auth, requireRole('admin'));
Tips
- Keep middlewares small. One job per middleware.
- Always call next() or send a response. Forgetting causes hangs.
- Do not put business logic in middleware. Middleware shapes the request; logic lives in handlers or services.
- Use factory functions for configurable middlewares (validate(schema), requireRole(role)).
The Takeaway
Write custom middleware with the (req, res, next) signature. Common patterns: auth, validation factories, request IDs, async wrappers, and permission factories. Keep middlewares small, always call next() or respond, and use factories for configurable middlewares.
Use the (req, res, next) signature. Do something with req or res, then call next() to continue or send a response to end the cycle. Keep middlewares small and one-job.
Write middleware for cross-cutting concerns (auth, validation, logging, request IDs, permissions). Put business logic in handlers or services. Middleware shapes the request; logic acts on it.
Use a factory function. The factory takes options and returns a middleware. Example: validate(schema) returns a middleware that validates against that schema. requireRole(role) returns a middleware that checks that role.
Express 4 does not catch promise rejections from async middlewares. Wrap with Promise.resolve(fn(req, res, next)).catch(next) so rejections go to the error handler instead of being swallowed.
Forgetting to call next() or send a response. The request then hangs. Every middleware must call next() or send a response. Also, putting business logic in middleware makes it hard to test and reuse.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

