API Request, Response, and Error Shapes Clients Can Trust
Design consistent API request and response envelopes, validation errors, and problem details so client developers can handle success and failure reliably.
Stable API contracts tell clients what to send and how to handle success or failure. This guide covers request validation, Problem Details, field errors, batch outcomes, gateway responses, and safe logging, with trade-offs between standard and custom envelopes. Readers can use these patterns to build clients that handle errors predictably without exposing sensitive details.
API Request, Response, and Error Shapes Clients Can Trust
Introduction
An API response is part of its contract: clients need to know which fields to send and how to distinguish a validation problem from a server failure. Consistent shapes make that behavior easier to document and handle, while keeping internal details out of public responses.
This guide uses HTTP Problem Details to model failures, then covers request validation, success responses, partial outcomes, gateway errors, and logging. It also weighs when a shared envelope helps and when it obscures useful HTTP semantics.
A practical error shape
The Problem Details format (application/problem+json) provides standard fields such as type, title, status, detail, and instance. APIs can add an errors extension for field-level validation while keeping a consistent envelope:
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{
"type": "https://api.example.com/problems/validation",
"title": "Request validation failed",
"status": 422,
"detail": "Correct the marked fields and try again.",
"instance": "/api/orders",
"code": "validation_failed",
"errors": [{"field":"quantity","code":"must_be_positive"}]
}
The body should not expose SQL, filesystem paths, stack traces, or secrets. Keep validation errors specific enough to fix, but avoid confirming sensitive account existence when that would create an enumeration risk.
Model the response path
sequenceDiagram
participant Client
participant API
participant Validator
participant Service
Client->>API: Request with contract fields
API->>Validator: Parse and validate
alt Invalid input
Validator-->>API: Field violations
API-->>Client: 4xx problem response
else Valid input
Validator-->>API: Typed input
API->>Service: Apply business operation
Service-->>API: Result
API-->>Client: 2xx representation
end
This boundary helps keep HTTP concerns out of domain code. The validator produces a known input shape; the service returns a domain outcome; the API layer maps outcomes to status codes and response formats.
When to use and when not to use
Use a consistent response contract for APIs consumed by multiple clients, especially when teams deploy clients independently from servers. Use field-level errors for forms and structured clients that can point to specific inputs. A tiny internal endpoint may not need a complex envelope; keep it simple while preserving a stable status and content type. Avoid a success-shaped body for failure, because intermediaries and generic client libraries rely on HTTP status semantics.
Trade-Off Table
| Design | Strength | Cost |
|---|---|---|
| Problem Details | Standard fields and clear media type | Extensions still need documentation |
| Custom stable error code | Straightforward client branching | Requires governance and code catalog |
Generic {data, error} envelope |
Uniform JSON shape | Can obscure status semantics and add wrappers |
| Raw framework error | Cheap during development | Leaks details and changes across versions |
Implementation snippet
Map known errors centrally, then keep unexpected failures generic:
function toProblem(error: unknown, requestId: string): Response {
if (error instanceof ValidationError) {
return Response.json(
{
type: "about:blank",
title: "Request validation failed",
status: 422,
code: "validation_failed",
errors: error.fields,
},
{
status: 422,
headers: {
"Content-Type": "application/problem+json",
"X-Request-Id": requestId,
},
},
);
}
return Response.json(
{
title: "Internal server error",
status: 500,
code: "internal_error",
requestId,
},
{ status: 500 },
);
}
The production handler should log the internal exception with the request ID, while returning only safe details to the caller.
Production failure scenarios and mitigations
A frontend depends on an error message string and breaks after copy editing. Give clients stable codes and treat detail as display text. A gateway replaces a JSON error with an HTML timeout page; document gateway behavior and let clients handle invalid response bodies defensively. A batch endpoint returns 200 when individual items failed, but provides no per-item status; define partial success explicitly with per-item outcomes or make the whole request atomic. A deployment changes field names without a migration window; use additive changes and contract checks.
Observability checklist
- Track status, stable error code, route template, request ID, and latency.
- Measure validation failures by field code without recording submitted private values.
- Separate expected client errors from unexpected server errors in alerts.
- Monitor response content type and body size, including gateway-generated errors.
Security and Compliance Notes
Error bodies are public output. Redact credentials and personal data, sanitize reflected input, and avoid stack traces. Return the same auth error where revealing whether an account exists would be unsafe. Apply the same care to logs: restrict access, define retention, and avoid storing full request bodies when they may contain personal or regulated data. If a response includes user supplied text, encode it for its eventual display context so an error message cannot become an injection path.
Common Pitfalls / Anti-Patterns
- Returning a different error schema from each route forces clients to maintain endpoint-specific parsers.
- Changing or reusing error codes makes client handling unreliable; treat documented codes as part of the API contract.
- Using
200 OKfor a failed operation breaks generic HTTP handling and hides failures from caches and monitoring. - Returning an empty
500without a request identifier leaves operators with no useful way to find the protected server-side exception. - Logging complete request bodies can expose credentials and personal data. Log safe fields and a correlation ID instead.
Quick Recap Checklist
- Document request fields, success payloads, and error payloads together.
- Return meaningful HTTP status codes and stable machine-readable error codes.
- Keep public error details safe while recording useful diagnostics in protected logs.
- Include a request identifier that connects a client response to server-side traces or logs.
Interview Questions
validation_failed provides a stable machine-readable condition.
Further Reading
- RESTful API Design — Resource modeling and consistent HTTP operations.
- API Contracts — OpenAPI descriptions and compatibility checks.
- RFC 9457: Problem Details for HTTP APIs — Standard fields and media type for machine-readable HTTP errors.
- OpenAPI Specification — Describe response schemas, status codes, and examples in an API contract.
- OWASP API Security: Security Misconfiguration — Risks from verbose errors and unsafe defaults.
Conclusion
A stable request-response contract reduces defensive code in every client. Keep the envelope small, use HTTP status codes honestly, and separate public diagnostics from internal exceptions.
Category
Related Posts
API Resource Names, Relationships, and Collections
Model API URLs around stable resources and relationships, with clear collection behavior that clients can navigate without learning server internals.
HTTP Methods, Status Codes, and Headers in API Design
Choose HTTP methods that match the operation, return status codes clients can act on, and use headers for metadata, caching, and safe retries.
API Examples, Schemas, and Useful Error Documentation
Write API examples, schemas, and error docs that help developers send valid requests, handle failures, and understand exactly what a response means.