Most Express tutorials show one route that returns JSON and stop there. The real trouble starts when an async database call rejects, a request body arrives empty, or two branches try to send the same response. A reliable API needs a deliberate middleware order and one error path that every failure can reach.
A small courses resource shows all of it: two routes, a JSON parser, a catch-all for unmatched URLs, and one error handler that every failure reaches.
The Express.js request lifecycle
An Express application is an ordered chain of middleware. Each middleware receives req, res, and next. It must either finish the request by sending a response, or call next() so Express can continue along the chain.
Registration order is execution order. That simple rule has practical consequences. If express.json() is registered after a router, the router sees the request first and req.body will be undefined. If a catch-all 404 handler is registered before valid routes, it will intercept requests that should have matched those routes.
Think of the application as a pipeline, not a collection of unrelated functions. Parsing prepares the request, logging observes it, routing handles it, and the final middleware deal with unmatched URLs or errors.
Routing a REST resource
An express.Router() keeps related endpoints together. For a courses resource, two basic routes are:
GET /api/courses/:idto fetch one coursePOST /api/coursesto create one course
Express exposes inputs in three places. Path values such as :id are in req.params. Optional URL filters such as ?level=beginner are in req.query. JSON submitted by the client is in req.body, provided the JSON parser ran first.
The response status should describe the result, not merely whether the handler executed. A found resource returns 200. A newly created resource returns 201. Invalid input returns 400. A missing resource returns 404. An unexpected server failure falls back to 500.
A fully worked Express API example
Start with the pieces. An error type carries a status, a wrapper forwards async rejections, and the router holds the two routes:
const express = require('express');
const app = express();
class HttpError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
const asyncHandler = fn => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
const coursesRouter = express.Router();
coursesRouter.get('/:id', asyncHandler(async (req, res) => {
const course = await Course.findById(req.params.id);
if (!course) throw new HttpError(404, 'Course not found');
res.json(course);
}));
coursesRouter.post('/', asyncHandler(async (req, res) => {
const { title } = req.body;
if (!title) throw new HttpError(400, 'title is required');
const course = await Course.create({ title });
res.status(201).json(course);
}));
function requestLogger(req, res, next) {
console.log(req.method, req.originalUrl);
next();
}
function notFound(req, res, next) {
next(new HttpError(404, `No route for ${req.method} ${req.originalUrl}`));
}
function errorHandler(err, req, res, next) {
res.status(err.status || 500).json({ error: err.message });
}Then the wiring, which runs the moment the file loads, so it sits below the definitions:
app.use(express.json()); // 1 body parser
app.use(requestLogger); // 2 logging
app.use('/api/courses', coursesRouter); // 3 routes
app.use(notFound); // 4 catch-all 404
app.use(errorHandler); // 5 error handler, 4 args, LASTNow trace four requests.
GET /api/courses/999reaches the GET route. Suppose no course has that ID. The route throwsHttpError(404, 'Course not found').asyncHandlerconverts the rejected promise intonext(err), so Express skips ordinary middleware and invokeserrorHandler. The response is404 { "error": "Course not found" }.POST /api/courseswith body{}passes throughexpress.json(), soreq.bodyis an empty object. The validation check throwsHttpError(400, 'title is required'). The same error path returns400 { "error": "title is required" }.POST /api/courseswith body{ "title": "DSA" }passes validation.Course.createreturns the saved object, and the route responds with201 { "_id": "...", "title": "DSA" }.GET /api/studentsmatches no route, so the chain falls through tonotFound. That middleware raisesHttpError(404, 'No route for GET /api/students')and passes it tonext, so the reply is404 { "error": "No route for GET /api/students" }. A missing record and a missing route both end as 404, but they are raised at different points in the chain and only the second one touchesnotFound.

The important design is not the database model. It is that validation failures, missing records, and rejected operations all converge on one response-producing function.
Error handling the production way
Express recognises error middleware by its four parameters: (err, req, res, next). Even when the function does not use next, keep it in the signature. Remove the fourth parameter and Express treats the function as ordinary middleware, so it will not receive forwarded errors.
Register the error handler after every route and after the not-found handler. A normal request should never reach it. An error passed through next(err) jumps past ordinary middleware and lands there.
For Express 4, an async rejection is not forwarded automatically, which is why the example wraps each async route. Promise.resolve(...).catch(next) turns both a thrown error and a rejected promise into the same next(err) call. Express 5 can forward a rejection from a returned async handler, but the explicit wrapper remains common in Express 4 codebases. Know which major version the project uses.
An HttpError carrying a status keeps route code focused. A route throws new HttpError(404, message) or new HttpError(400, message), while the global handler decides the JSON shape. Unknown errors naturally use 500.
Validation and the double-response trap
Validate inputs at the top of a handler, before database work. Throwing a 400 early makes the rest of the handler operate on known-good data.
Another common failure is Cannot set headers after they are sent. It means one branch already called res.json() or res.send(), but execution continued and another branch tried to respond again. This version is broken:
if (!course) res.status(404).json({ error: 'Not found' });
res.json(course);Return when the response ends that handler:
if (!course) return res.status(404).json({ error: 'Not found' });
return res.json(course);When using the central error approach, throw instead of sending the first response. Whichever style you choose, one request must receive one response.
Express traps interviewers and code review catch
These defects all follow from the lifecycle above:
Error middleware placed before routes cannot catch errors raised later in the chain.
A three-parameter "error handler" is silently treated as normal middleware.
An Express 4 async rejection that is not forwarded can become an unhandled rejection and leave the client waiting.
express.json()placed after the router leaves JSON route handlers without a parsed body.Sending a response and continuing creates a second-response attempt.
A useful interview method is to write the middleware in order first, then trace normal control with next() and error control with next(err). That exposes most mistakes before you run the code.
How interviews test Express routing and errors
Typical questions ask you to order middleware, explain why an async route does not reach the error handler, write a global handler, or select the correct status for a result. There is no exam syllabus to memorise here. The answer comes from Express control flow and HTTP semantics.
For broader preparation, Node.js Interview Questions for Freshers connects routing to runtime behaviour, while JavaScript Interview Questions for Freshers 2026 reinforces the promise mechanics behind async handlers.
The short version and next step
Middleware runs in registration order. Parse before routing, wrap Express 4 async handlers so rejections become next(err), validate early, and let one four-argument error handler registered last produce the failure response. Always return after sending a response.
KnowledgeGate's question bank carries about 600 MERN-stack practice questions. Build the backend with NodeJs + ExpressJs + MongoDB, place it in a wider path with MERN Stack + DSA, or work through the Node.js lessons in the MERN Stack course before choosing your next topic.




