Centralized error handling, input validation with Zod, custom error classes, and proper HTTP status codes.
// Error handler middleware: 4 params = Express treats it as error handler
module.exports = (err, req, res, next) => {
console.error(err.stack);
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ error: 'Validation failed', details: errors });
}
// MongoDB duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
return res.status(400).json({ error: `${field} already exists` });
}
// JWT errors
if (err.name === 'JsonWebTokenError') return res.status(401).json({ error: 'Invalid token' });
// Default
res.status(err.status || 500).json({ error: err.message || 'Internal server error' });
};// Error handler must be last middleware
app.use(errorHandler);
// In async routes, pass errors to next()
router.get('/posts/:id', async (req, res, next) => {
try {
const post = await Post.findById(req.params.id);
if (!post) return res.status(404).json({ error: 'Not found' });
res.json(post);
} catch (err) {
next(err); // sends to errorHandler
}
});npm install zod
const { z } = require('zod');
const createUserSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
password: z.string().min(8),
});
// Middleware
function validate(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ errors: result.error.flatten() });
}
req.validatedBody = result.data;
next();
};
}
router.post('/users', validate(createUserSchema), async (req, res) => {
// req.validatedBody is clean and type-safe
});(err, req, res, next) is Express's error handler. Register it last.next(err) in async routes sends errors to the error handler — don't swallow them.safeParse never throws — check result.success.