# Your API Does Not Need More Logs. It Needs One Error Shape.

Your API does not need more logs. It needs one error shape.

A client I worked with kept breaking every time an endpoint failed. One error came back as plain text, the next as an HTML page, the next as a JSON object with a different field name each time. The frontend could not tell a missing order from a server crash, so it showed the same vague message for both.

The frontend team had a different check for every endpoint, and every new endpoint added another guess. Support tickets said the same thing each time: "something went wrong." Nobody could say what, because the API never said what in a shape the client could read.

## The wrong fix

The wrong fix is to write a better message for each endpoint.

Better messages in five different shapes are still five different shapes. A clear message inside a shape the client cannot parse is still unreadable to the client. Every new endpoint adds one more shape for the client to guess, and the frontend grows a new special case for each one.

## The real fix: one shape for every error

Every error returns the same shape. It has a stable code the client can check and a message a human can read:

```js
return res.status(404).json({
  error: {
    code: 'ORDER_NOT_FOUND',
    message: 'No order exists for this id.',
  },
});
```

The code never changes for the same failure, and the message never decides what the client does. The client checks `error.code`, not the message text. A missing order shows a clear message, a server error shows a retry option, and a new endpoint needs no new error handling on the client.

### One place that builds the shape

Do not build this object by hand in every route. Build it once, in one helper, and let every route use it:

```js
function sendError(res, status, code, message, details) {
  return res.status(status).json({
    error: {
      code,
      message,
      ...(details ? { details } : {}),
    },
  });
}
```

Then the routes stay short, and the shape stays the same:

```js
app.get('/orders/:id', async (req, res) => {
  const order = await db.orders.findById(req.params.id);
  if (!order) {
    return sendError(res, 404, 'ORDER_NOT_FOUND', 'No order exists for this id.');
  }
  return res.json({ order });
});
```

### Validation errors use the same shape

A validation failure is not a different kind of error. It uses the same shape, with a `details` field that lists what failed:

```js
return sendError(res, 400, 'VALIDATION_FAILED', 'The order data is not valid.', [
  { field: 'customerId', message: 'Customer id is required.' },
  { field: 'items', message: 'At least one item is required.' },
]);
```

The client still checks `error.code` first. The details field only tells the form which fields to highlight.

### Server errors use the same shape too

A 500 is the shape doing its most useful work. The client sees a stable code and a safe message, while the real error goes to your logs, not to the response:

```js
app.use((err, req, res, next) => {
  logger.error('unhandled_error', {
    path: req.path,
    message: err.message,
    stack: err.stack,
  });

  return sendError(res, 500, 'INTERNAL_ERROR', 'Something went wrong on our side. Please try again.');
});
```

The user gets a retry option instead of a stack trace. Support gets a log line with the real cause. The shape is identical in both cases.

## What the shape must never do

Two rules keep the shape honest.

First, never change the code for the same failure. If `ORDER_NOT_FOUND` means a missing order today, it must mean a missing order next year. The moment a code changes meaning, every client check built on it breaks quietly.

Second, never put internal detail in the message. A message that names a table, a query, or a file path hands an attacker a map of your system. The message is for the human in front of the screen. The cause belongs in your logs.

## How the client uses it

Once the shape is stable, the client code gets simple. One check, one switch, no guessing:

```js
const res = await fetch(`/orders/${id}`);
if (!res.ok) {
  const body = await res.json();
  if (body.error.code === 'ORDER_NOT_FOUND') {
    showMessage('This order does not exist.');
  } else if (body.error.code === 'INTERNAL_ERROR') {
    showRetry('The server had a problem. Try again.');
  }
  return;
}
```

A new endpoint needs no new error handling on the client. It returns the same shape, so the client already knows how to read it.

---

Before you add another log line, check what your errors actually return. How many shapes does your API have today? Drop it in the comments.

---

*I'm Sohail Khan, a Senior Full Stack Developer building AI agents and automation. I write about backend engineering, AI integration and practical system design.*
