Full Stack Developer specializing in React, Node.js, and scalable web solutions.

I am Ujjawal Solanki, a dedicated software developer with expertise in full-stack development, React applications, and Node.js backend services. I create innovative, scalable, and user-friendly web applications.

Node.js Express Middleware Guide

I Ignored Express Middleware. Then One Bug Spread Everywhere

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.

What middleware finally became in my head

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.

The old way that kept hurting me

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.

The middleware stack I use most often

Step 1

Start with request logging in development

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);
Step 2

Move authentication into one reusable function

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' });
  }
}
Step 3

Add role or ownership checks as separate middleware

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);
Step 4

Validate request bodies before controllers touch them

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();
}
Step 5

End the chain with one error handler

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'
  });
});

Troubleshooting the middleware chain

  • If `next()` is never called, the request hangs and looks mysteriously dead.
  • If middleware order is wrong, validation or auth can run too late.
  • If a middleware both sends a response and still calls `next()`, double-response bugs appear.
  • If you attach `req.user` inconsistently, downstream code becomes fragile.

Good middleware is boring in the best way. It makes requests predictable, which makes systems safer.

What changed after I embraced middleware

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.