Guides
Errors
Every error uses the same JSON shape and an accurate HTTP status code, so you can write one error handler for the whole API.
Error format
404 Not Found
{
"message": "User with id 999999 not found",
"status": 404,
"code": "RESOURCE_NOT_FOUND",
"path": "/api/v1/users/999999",
"requestId": "6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73"
}| Field | Description |
|---|---|
| message | Human-readable explanation. Safe to show to developers; do not parse it. |
| status | The HTTP status code, repeated for convenience. |
| code | Stable machine-readable error code. Branch on this. |
| path | The request path (without query string). |
| requestId | Matches the X-Request-Id response header. Include it in bug reports. |
| errors | Only for validation and conflict errors: a list of { path, message } per field. |
Validation errors
422 responses list every problem at once, keyed by field path, which maps neatly onto form field errors:
422 Unprocessable Entity
{
"message": "Validation failed: email: email is required",
"status": 422,
"code": "VALIDATION_ERROR",
"path": "/api/v1/users",
"requestId": "6f1c2a9e-3b7d-4c51-9a0e-2d8f4b6c1e73",
"errors": [
{
"path": "email",
"message": "email is required"
}
]
}Status and error codes
| Status | Code | Meaning |
|---|---|---|
| 400 | INVALID_PARAMETER | A query or path parameter is missing, unknown, or out of range. |
| 400 | INVALID_ID | A path ID is not a positive integer. |
| 400 | INVALID_JSON | The request body is not valid JSON. |
| 401 | MISSING_TOKEN / INVALID_AUTH_HEADER | Authorization header missing or malformed. |
| 401 | INVALID_TOKEN / TOKEN_EXPIRED | Bearer token invalid or expired. |
| 401 | INVALID_CREDENTIALS | Login failed. |
| 403 | FORBIDDEN | Authenticated, but not allowed. |
| 404 | RESOURCE_NOT_FOUND | No record with that ID (or category). |
| 404 | ROUTE_NOT_FOUND | No endpoint at that path. |
| 405 | METHOD_NOT_ALLOWED | The path exists but not for this method. See the Allow header. |
| 409 | CONFLICT | A unique value (email, username) is already taken. |
| 413 | PAYLOAD_TOO_LARGE | The body exceeds 100 KB. |
| 415 | UNSUPPORTED_MEDIA_TYPE | The body is not sent as application/json. |
| 422 | VALIDATION_ERROR | The JSON body is well-formed but fails validation. See the errors array. |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests. Wait for Retry-After seconds. |
| 500 | INTERNAL_ERROR | Unexpected server error. No internal details are exposed. |
| 503 | SERVICE_UNAVAILABLE / REQUEST_TIMEOUT | The database is unavailable or the request took too long. Retry later. |
Handling errors in your client
- Check the HTTP status first.
fetchdoes not throw on 4xx/5xx; Axios and Dio do. - Branch on
code, not onmessage. - Retry only
429and503, after theRetry-Afterdelay. - Want to test your error UI? /test/status/:code returns any common status on demand.