Best Practices for RESTful API Error Payloads
Designing predictable, informative error payloads is essential for building robust REST APIs and ensuring seamless client integration. This article outlines the essential best practices for structuring RESTful API error payloads, focusing on HTTP status code usage, consistent schema design, machine- and human-readable formatting, input validation reporting, security concerns, and established industry standards such as RFC 7807.
Align HTTP Status Codes with Response Payloads
An error response should never return an HTTP 200 OK
status with an error message in the body. Use standard HTTP status codes
to communicate the high-level category of the issue:
- 4xx (Client Errors): Indicate that the client sent
an invalid request (e.g.,
400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found,422 Unprocessable Entity). - 5xx (Server Errors): Indicate that the failure
occurred on the server side (e.g.,
500 Internal Server Error,502 Bad Gateway,503 Service Unavailable).
The payload should complement the HTTP status code by providing granular context rather than overriding the transport layer.
Maintain a Predictable, Consistent Schema
Every error returned by your API should share a uniform JSON structure, regardless of whether the error originated from an authentication layer, validation middleware, or an unhandled exception.
A baseline payload should include:
status: The HTTP status code repeated in the body for convenience.code: A machine-readable, stable error identifier (e.g.,INVALID_CREDENTIALS).message: A human-readable summary of the issue intended for the developer.timestamp: An ISO 8601 formatted timestamp indicating when the error occurred.trace_id(orrequest_id): A unique tracking identifier to correlate client requests with backend logs.
{
"status": 404,
"code": "RESOURCE_NOT_FOUND",
"message": "The requested user does not exist.",
"timestamp": "2023-10-27T10:15:30Z",
"trace_id": "req-9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d"
}Distinguish Machine-Readable and Human-Readable Information
Client applications rely on stable string constants or error codes to implement programmatic control flow (such as routing to a specific UI state). Human-readable messages change frequently for clarity and should not be parsed by code.
- Use
codefor deterministic client logic (e.g.,EMAIL_ALREADY_IN_USE). - Use
messagefor debugging assistance or developer context. - If internationalization (i18n) is required, let the client localize
text based on the
code, or provide localized strings via dedicated localized fields.
Provide Granular Details for Validation Failures
When handling client validation errors (typically with a
400 Bad Request or 422 Unprocessable Entity),
a single message is insufficient. APIs must return an array of specific
field errors so frontend applications can map issues directly to user
forms.
{
"status": 422,
"code": "VALIDATION_FAILED",
"message": "Validation failed for one or more fields.",
"errors": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "The email address provided is not valid."
},
{
"field": "password",
"code": "PASSWORD_TOO_SHORT",
"message": "Password must be at least 8 characters long."
}
],
"trace_id": "req-4a8f9c1e-6c2a"
}Consider Adopting RFC 7807 (Problem Details)
Rather than inventing a proprietary structure, consider adopting the
RFC 7807 (Problem Details for HTTP APIs) specification
(using the application/problem+json media type). This
standard defines a common format:
type: A URI reference that identifies the problem type.title: A short, human-readable summary of the problem type.status: The HTTP status code.detail: A human-readable explanation specific to this occurrence of the problem.instance: A URI reference identifying the specific occurrence of the problem.
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 403,
"detail": "Your current balance is $30, but the transaction costs $50.",
"instance": "/accounts/12345/transactions/abc",
"balance": 30
}Sanitize Payloads for Security
Never expose internal implementation details in production environments. Stack traces, database error messages, table names, and server frameworks expose attack vectors to malicious actors.
- Development: Return full stack traces and debug metadata to accelerate local development.
- Production: Log internal errors privately alongside
a
trace_id, and return only generic, high-level messages to the client.