REST API design
REST API error response best practices
A useful API error tells a client what failed, why it failed, and what to do next—without exposing private implementation details. A stable error contract also makes integrations easier to test and safer to upgrade.
Start with the correct HTTP status
The status code provides the broad outcome; the body provides the details. Use a specific 4xx code for a request the client can correct and a 5xx code for a failure owned by the server or its dependencies.
400The request is malformed or fails general validation.
401 / 403Authentication is missing or the authenticated caller lacks permission.
404The requested route or resource cannot be found.
409The operation conflicts with the resource's current state.
422The request is understood but contains field-level validation problems.
429The caller exceeded a rate limit and may need retry guidance.
500 / 503The service failed unexpectedly or is temporarily unavailable.
Clients, monitoring systems, and gateways rely on status codes. A success status containing an error message creates ambiguous behavior.
Use one predictable error shape
Clients should not need a different parser for every endpoint. Define one envelope and keep its required fields stable across your API.
{
"error": {
"code": "INVALID_INPUT",
"message": "One or more fields are invalid.",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Enter a valid email address."
}
],
"requestId": "req_7f31a2"
}
}The exact property names can differ, but the meaning of each property should remain consistent. Document which fields are always present and which are optional.
Separate machine codes from human messages
A machine-readable code such as INVALID_FORMAT is stable enough for application logic. A message is written for people and may be clarified, translated, or reworded later.
Clients should branch on the status and error code—not on exact message text. Test codes exactly, while testing messages only for presence and usefulness unless the wording is part of a formal contract.
Return useful validation details
When several fields are invalid, returning all known problems in one response saves extra request cycles. Each detail can identify the field or JSON path, a stable validation code, and a concise explanation.
Be precise enough to help the caller fix the request. “Invalid input” alone is rarely actionable; “email must contain a valid address” is much more useful.
Include a request or trace identifier
A request ID connects the public error to internal logs without publishing those logs. Support teams can ask for this value, locate the failed request, and investigate it quickly.
Generate the identifier at the system boundary and pass it through downstream services. Return it consistently in the error body or a documented response header.
Do not expose sensitive internals
Public responses should not contain stack traces, SQL statements, filesystem paths, access tokens, secret configuration, or unfiltered dependency errors. Log diagnostic detail securely and return a safe summary to the caller.
{
"error": "SQLSTATE 42P01",
"query": "SELECT * FROM...",
"stack": "at db/query.js:82"
}{
"error": {
"code": "INTERNAL_ERROR",
"message": "The request could not be completed.",
"requestId": "req_7f31a2"
}
}Give safe retry guidance
For rate limits and temporary outages, tell clients when retrying may help. Use the documented Retry-After header where appropriate, and recommend exponential backoff with jitter for automated clients.
Do not imply that every failure is retryable. Validation, authentication, permission, and conflict errors normally require the request or resource state to change first.
Keep errors backward compatible
Changing an error status, removing a field, renaming a machine code, or changing a value's type can break a client just as easily as changing a success response. Add optional fields when possible and version unavoidable breaking changes.
During a migration, send the same failing requests to both API versions. Compare status codes, body structure, machine codes, field paths, headers, and response time.
Error-response review checklist
- The HTTP status matches the type of failure
- Every endpoint uses the documented error envelope
- Machine codes are stable and documented
- Validation details identify fields clearly
- A request ID supports troubleshooting
- Messages do not expose private internals
- Retryable errors provide safe guidance
- Old and new versions preserve client contracts
Compare failure cases before release
Run representative invalid and failing requests against both endpoints, then review each status, error body, header, and timing difference.
Compare API responses