© Ujjawal Solanki | All Rights Reserved
For an embarrassingly long time, I treated Express middleware like optional ceremony. I knew it existed. I just kept telling myself I could add it later.
So I placed auth checks inside controllers, validated payloads wherever I remembered, and logged requests only when I was already confused.
The system looked fine until one unprotected route slipped through. That single miss taught me how expensive it is to scatter cross-cutting logic across the app.
Middleware is not magic. It is just a sequence of reusable checks and transformations before your final handler runs.
Once I started thinking of requests as walking through a hallway, the pattern felt obvious. Logging door. Auth door. Validation door. Controller room.
Middleware is where repeatable rules belong. If the same logic appears in many controllers, that is usually your clue.
My JWT check was copy-pasted into multiple controllers. When I improved the logic, I updated some copies and missed others.
That kind of duplication feels harmless until security or validation rules change. Then every forgotten copy becomes a bug waiting for traffic.
Before I chase fancy observability, I want to know what route was hit, with what method, and in what order things happened.
function requestLogger(req, res, next) {
console.log(`${req.method} ${req.originalUrl}`);
next();
}
app.use(requestLogger);Authentication logic is a terrible place for copy-paste. A single middleware makes the protection explicit and reduces silent drift.
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({ message: 'Unauthorized' });
}
try {
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
} catch {
return res.status(401).json({ message: 'Invalid or expired token' });
}
}This was another important lesson for me. Auth says who the user is. Authorization says what they may do. Those are related, but not identical.
function requireRole(role) {
return (req, res, next) => {
if (req.user?.role !== role) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
};
}
router.delete('/:id', requireAuth, requireRole('admin'), deleteUser);Validation middleware keeps controllers focused on useful work. It also makes failure cases more readable because bad input stops at the boundary.
function validateProject(req, res, next) {
if (!req.body.title || !req.body.title.trim()) {
return res.status(400).json({ message: 'Title is required' });
}
next();
}When I did not have a final error handler, random exceptions turned into inconsistent crashes or ugly responses.
A shared error middleware gives the app one last chance to respond cleanly and log what matters.
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({
message: err.message || 'Internal server error'
});
});Good middleware is boring in the best way. It makes requests predictable, which makes systems safer.
My controllers got shorter. My auth story became easier to explain. Fixing one shared rule no longer required hunting through a dozen route files.
Middleware did not make Express more complex. It removed complexity that I had been hiding in the wrong places.
If this helped you, save it for later. And if you are stuck on a MERN feature, you can always reach out to me.
© Ujjawal Solanki | All Rights Reserved