Chapters of From Frontend to Full-Stack Backend

Week 1 - API FoundationChapter 3 of 282 min read

Middleware in depth - the full journey of a request

Built-in, third-party and custom middleware, passing data on the req object, route-level middleware

The idea of middleware came up in the last chapter. Today let us look one level deeper, because in interviews the question 'how does middleware work' is almost guaranteed. From the moment a request enters the server until the response goes out, almost everything happens in middleware's hands.

Express has three kinds of middleware. Built-in - like express.json(), without which req.body always comes back undefined. Third-party - like cors or morgan. And custom - the ones you write yourself, like an auth check or a role check.

Passing data from middleware to the controller

A nice pattern is this: the auth middleware pulls the user out of the token and puts it on req.user, then the controller uses that user. That way no controller has to decode the token again and again.

function requireAuth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (!token) return res.status(401).json({ success: false, message: "Login required" });

  const payload = verifyToken(token); // { id, role }
  req.user = payload;
  next();
}

// Auth is needed only on this route
router.get("/profile", requireAuth, profileController);
router.post("/orders", requireAuth, requireRole("user"), createOrder);

Notice - middleware can also sit at route level. Not every route needs auth; put it only where it is needed. One more thing: a middleware can stop the chain right here if it wants - like sending a 401 when there is no login. Once you have sent a response there is no need to call next(); in fact, calling it then would be a mistake.

The async middleware trap

If a middleware is async and an error happens inside it, Express cannot catch it on its own (in Express 4). You either have to wrap it in try/catch and send next(err), or use a small wrapper. This one-line wrapper makes every controller in the whole project safe.

const asyncHandler = (fn) => (req, res, next) => {
  Promise.resolve(fn(req, res, next)).catch(next);
};

// Usage
router.get("/users", asyncHandler(async (req, res) => {
  const users = await userService.getAll();
  res.json({ success: true, data: users });
}));

The mistake everyone makes

The most visible mistake: putting express.json() after the routes. Then every POST request gets req.body as undefined, and the developer spends hours hunting for the fault. The body parser always goes before the routes.

Today's practice

Write a requireAuth middleware in your project (for now just check whether a token exists in the header; real JWT comes in the later chapters). Build a protected route and test it with Postman or curl - both ways, with a token and without one.

How to say it in an interview

Middleware is a step in the request pipeline - each step either calls next() and moves forward, or sends a response and stops the chain. I keep cross-cutting work like auth in middleware, and I keep controller work light by putting the user info from the token on req.user.

All chapters of From Frontend to Full-Stack Backend