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 (or request_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 code for deterministic client logic (e.g., EMAIL_ALREADY_IN_USE).
  • Use message for 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.