Error-contract answer
A good API error response has a stable shape, a useful code, a human-safe message, and optional details for validation errors.
Separate machine decisions from human wording
Developers search this when frontend code becomes messy because every endpoint fails differently.
Fields that stay stable
Errors are part of your API contract. If they are random, every client has to guess what went wrong.
| Field | Purpose |
|---|---|
| code | Stable machine-readable reason |
| message | Safe human-readable explanation |
| details | Optional field-level errors |
| requestId | Support/debug lookup |
Return a withdrawal rejection safely
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Please check the highlighted fields.",
"details": {
"email": "Enter a valid email address."
},
"requestId": "req_123"
}
}
This example is intentionally small. In a real codebase, the surrounding details matter: naming, error handling, tests, runtime config, permissions, and how easy the next developer can understand the change.
Map domain errors at one boundary
- Use one error envelope.
- Keep internal stack traces out of responses.
- Return validation details in a predictable format.
- Include request IDs for support.
- Document common error codes.
Error payloads that leak or drift
- Returning raw exception messages.
- Changing error shapes per endpoint.
- Using only HTTP status without a code.
- Leaking database or provider details.
- Making messages too vague for users to act.
Document the client contract
Use a concrete sentence:
I used this pattern because [problem]. The main tradeoff was [tradeoff]. I verified it by [test or check].
That structure works because it shows judgment. Anyone can name a tool. Strong developers explain why they chose it, what could go wrong, and how they checked the result.
Related API guides
- nodejs backend project structure without overengineering
- request id logging nodejs production debugging
- zod validation api boundaries typescript
HTTP references
Make every failure traceable
A good API error response has a stable shape, a useful code, a human-safe message, and optional details for validation errors. Keep the implementation small, verify the edge cases, and write the decision down so the next person can trust it.
Keep one public shape across expected failures
An API client should not parse a PostgreSQL message on one route and a plain string on another. Map internal failures to a small public contract:
{
"error": {
"code": "INSUFFICIENT_FUNDS",
"message": "The available balance is too low for this withdrawal.",
"requestId": "req_01J...",
"details": []
}
}
In CryptoEx, the code is stable enough for a client decision, the message is safe to display, and the request ID connects the response to internal logs. The payload does not reveal the SQL query, provider response, account ledger, stack trace, or another user’s data.
Centralize translation at the transport boundary
Domain code should throw or return typed failures such as InsufficientFunds, WithdrawalAlreadyExists, or ProviderUnavailable. An HTTP error mapper chooses status, public code, and message. A queue consumer can map the same domain failure to retry, dead-letter, or manual review without pretending it is an HTTP response.
Validation details should identify fields with stable keys, not echo secrets or entire submitted objects. Log the internal cause once with the request ID. Do not both log and rethrow at every layer, which creates noisy duplicate traces and makes one failure look like five incidents.
Document status codes and public error codes beside each endpoint, then add contract tests so a refactor cannot quietly change the client-facing shape.
Questions for an error-contract review
Can a client branch on a stable code without reading English text? Does every response carry a request ID? Are validation details structured consistently? Could any message expose SQL, stack traces, secrets, internal service names, or another user’s state? Is retry guidance represented by status and headers rather than vague wording?
Test the contract from a real client. Expected failures should be boring to handle. Unexpected failures should remain safe for the user and rich enough in internal logs for an operator to investigate.
Discussion
What would you try, change, or challenge after reading this guide? Specific results and errors help the next reader.
Comments will load as you reach this section.