Building REST APIs with Node.js and Express That Don't Fall Apart at Route 40

TIL that an Express API without a consistent response/error shape is fine at 10 routes and a real liability at 40 — the fix was boring middleware, not a framework change.

2 min read

Sabin Shrestha

Full-Stack Developer — Next.js, React & React Native

The Problem #

Early routes on the Archery Garage backend each handled their own error responses inline — some returned { error: "message" }, others returned a raw string, one returned a 500 with no body at all. The client had no reliable way to read an error without special-casing individual endpoints.

Context #

The API grew organically: a route was added whenever the app needed one, with no shared conventions written down anywhere, because early on there were only a handful of endpoints and consistency didn't seem to matter yet.

What I Tried #

Went through and manually "fixed" each route's error response to match a format I'd decided on, one at a time, as I noticed the drift.

What Went Wrong #

New routes kept getting added the old way, because the "format" only existed as a convention in my head, not as anything enforced by the code. The manual fixes just meant the drift kept happening at a slower rate.

The Solution #

Moved response and error shaping into middleware instead of leaving it up to each handler. Route handlers just throw or return data; a central error middleware and a response wrapper handle formatting once.

// A route only does its own job
app.get("/orders/:id", asyncHandler(async (req, res) => {
  const order = await getOrder(req.params.id);
  if (!order) throw new ApiError(404, "Order not found");
  res.json({ data: order });
}));
 
// One place formats every error the same way
app.use((err, req, res, next) => {
  const status = err.status ?? 500;
  res.status(status).json({ error: { message: err.message, status } });
});

Why It Works #

A new route can't drift from the convention because the convention isn't optional per-route code — it's the only path an error or response can take out of the app. Consistency becomes a property of the architecture instead of something I have to remember.

Lessons Learned #

"I'll just be consistent" doesn't survive contact with a growing route count. If a convention matters, it has to be structurally enforced, not remembered.

What I Would Do Differently #

I'd set up the error middleware and an asyncHandler wrapper before writing the second route, not after noticing the third inconsistent error shape.

Centralized error handling middleware, consistent API response envelopes, asyncHandler patterns for async route handlers.