Middleware & Routing Interview Questions
Middleware and routing are the heart of Express, and they are where most interview questions get sharp. Interviewers probe whether you understand the request pipeline as an ordered chain of functions, how next() controls flow, and how the Router keeps large apps modular. The questions below cover the concepts that come up again and again, each with a precise, runnable answer.
What is middleware in Express?
Middleware is a function with the signature (req, res, next) that sits in the request/response pipeline. It can inspect or mutate req and res, end the cycle by sending a response, or pass control to the next function by calling next(). Express executes registered middleware in the order it was added, which is the single most important fact to internalize.
import express from "express";
const app = express();
app.use((req, res, next) => {
req.requestedAt = Date.now();
console.log(`${req.method} ${req.url}`);
next(); // hand off to the next middleware/route
});
app.get("/", (req, res) => {
res.json({ ok: true, requestedAt: req.requestedAt });
});
app.listen(3000);
Output:
GET /
What are the types of middleware?
Express recognizes several categories. Knowing the names signals fluency.
| Type | How it is registered | Typical use |
|---|---|---|
| Application-level | app.use() / app.METHOD() | Logging, body parsing, auth |
| Router-level | router.use() / router.METHOD() | Scoped behavior for a feature |
| Built-in | express.json(), express.static(), express.urlencoded() | Parsing, serving files |
| Third-party | app.use(cors()), app.use(helmet()) | Cross-cutting concerns |
| Error-handling | (err, req, res, next) => {} | Centralized error responses |
How does execution order work?
Middleware runs top to bottom in registration order, scoped by the path and HTTP method it was mounted on. A request flows through each matching layer until something sends a response or an error is thrown. If you register a body parser after a route that needs the body, the route runs first and the body is undefined — order bugs are common interview traps.
app.use(express.json()); // 1. parse body first
app.use("/api", (req, res, next) => { // 2. only for /api/*
console.log("api hit");
next();
});
app.post("/api/users", (req, res) => { // 3. body is now available
res.status(201).json({ created: req.body.name });
});
Tip: Mount path-scoped and security middleware (
helmet,cors, body parsers) before your routes, and mount error handlers last.
What is the difference between next() and next(err)?
Calling next() with no argument advances to the next regular middleware or route handler. Calling next(err) with any truthy argument tells Express to skip all remaining regular middleware and jump straight to the next error-handling middleware (the four-argument form). This distinction is a frequent question.
app.get("/orders/:id", (req, res, next) => {
if (!/^\d+$/.test(req.params.id)) {
return next(new Error("Invalid id")); // skip to error handler
}
next(); // continue normally
}, (req, res) => {
res.json({ id: req.params.id });
});
A special string, next("route"), skips the remaining handlers in the current route and moves to the next matching route — useful for branching logic.
How do you write error-handling middleware?
Error middleware is defined with exactly four arguments — (err, req, res, next). Express identifies it by arity, so the fourth parameter is required even if unused. It must be registered after all routes so that errors propagated by next(err) reach it.
app.get("/boom", (req, res, next) => {
next(new Error("Something failed"));
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({ error: err.message });
});
Output:
{ "error": "Something failed" }
Warning: In Express 4.x, errors thrown inside an async handler are NOT caught automatically — you must
try/catchand callnext(err), or wrap the handler. Express 5.x fixes this: a rejected promise returned from a handler is forwarded to the error middleware automatically.
// Express 4.x: manual forwarding
app.get("/user/:id", async (req, res, next) => {
try {
const user = await db.find(req.params.id);
res.json(user);
} catch (err) {
next(err);
}
});
What is the Express Router and why use it?
express.Router() creates an isolated, mountable mini-application. It supports the same .use() and .METHOD() API as app, letting you group related routes and their middleware into a separate module, then mount the whole group under a path prefix. This keeps large codebases organized and testable.
// routes/users.js
import { Router } from "express";
const router = Router();
router.use((req, res, next) => { // runs for every route in this router
req.scope = "users";
next();
});
router.get("/", (req, res) => res.json({ list: [] }));
router.get("/:id", (req, res) => res.json({ id: req.params.id }));
export default router;
// app.js
import users from "./routes/users.js";
app.use("/users", users); // GET /users, GET /users/:id
You can also chain methods for one path with router.route():
router.route("/:id")
.get((req, res) => res.json({ read: req.params.id }))
.put((req, res) => res.json({ updated: req.params.id }))
.delete((req, res) => res.status(204).end());
Best Practices
- Register middleware in deliberate order: security and parsers first, routes next, error handler last.
- Always either send a response or call
next()in every middleware path, or the request hangs. - Use
express.Router()to modularize features instead of stacking everything onapp. - Forward errors with
next(err)rather than callingres.send()from deep inside handlers. - In Express 4.x wrap async handlers in
try/catch; consider awrap()helper or upgrade to 5.x. - Define error middleware with all four arguments — Express detects it by parameter count.
- Keep middleware focused and single-purpose so the pipeline stays easy to reason about.