What structure should an API response use
Bottom line: Bare objects are clean until you need metadata; full envelopes are safe but verbose. For most projects use HTTP status for outcomes and a stable envelope for data.

Bottom line: express outcomes with 4xx/5xx status codes, carry the payload in { "data": ..., "meta": ... }, and switch to an error envelope on failure; that is enough for internal APIs, and only public platforms with many integrators need JSON:API or OpenAPI.
Rule out the worst option first: always returning 200 with the outcome in a code field.
Three shapes
A. Bare object
{ "id": 1, "name": "demo", "price": 99 }
Clean, and HTTP semantics do the work (404 is 404). But the moment you need pagination, warnings or a trace id, there is nowhere to put them without changing the contract.
B. Stable envelope
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "abc", "took": 12 }
}
Payload and metadata live apart, so new metadata never reshapes data. Costs one level of nesting.
C. Everything envelope (avoid)
{ "code": 0, "message": "success", "data": {...} }
The shape is not the sin; the practice is. Status 200 for every outcome means CDNs, gateways, circuit breakers, retry policies and dashboards all see success. Every alert then needs custom parsing.
The recommendation
HTTP status for the outcome, envelope for the payload.
{
"data": { "id": 1, "name": "demo" },
"meta": { "requestId": "req_8f2a", "took": 12 }
}
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order does not exist or was deleted",
"details": [{ "field": "id", "issue": "not_found" }]
},
"meta": { "requestId": "req_8f2a" }
}
Status 4xx or 5xx; code stable and machine-readable; message for humans; details for field-level form errors.
Hard rules
- List endpoints must carry pagination metadata. Without
totalor a cursor there is no pager. - Empty lists return
[], nevernull.null.mapis a recurring outage. - Timestamps in ISO 8601 with an offset. See what format for timestamps.
- One naming convention.
snake_caseorcamelCase, converted once at the boundary. - Money as integer minor units or strings.
99.99is not exact in binary floating point. - Do not conflate
nullwith absent. Seenullor omit the field.
When to adopt a specification
- Public platforms with many integrators: JSON:API or OpenAPI plus a shared envelope
- Single-team internal APIs: the envelope above is enough
- Strong contracts: Schema first, structures generated from it
Specifications save discussion and cost onboarding. For a five-person team, JSON:API is usually negative work.
FAQ
How should I choose between a bare object and an envelope?
Use an envelope when you need pagination, warnings or a requestId, so meta can grow without reshaping data. Return the bare object only when you need no metadata and can accept that adding fields later changes the contract.
Why should an API never return 200 for every outcome?
CDNs, gateways, circuit breakers, retry policies and dashboards all read the HTTP status to decide whether to cache, retry or alert. With 200 everywhere they treat failures as successes, and every alert needs custom parsing of the business code.
Should list endpoints carry pagination metadata?
Yes. Without total or a cursor there is no pager, and empty lists must return [] rather than null, or the frontend's .map call breaks.