Once you've settled on JSON as your format, the next question is how to shape the responses themselves. A handful of well-worn patterns cover almost every situation you'll run into, and picking the right one for each case keeps your API predictable to work with.
The single-resource response
Fetching one thing by ID should return that thing, flatly, without unnecessary wrapping:
GET /users/42
{
"id": 42,
"name": "Alice Chen",
"email": "alice@example.com",
"createdAt": "2026-01-15T10:30:00Z"
}
No envelope needed here — there's exactly one resource, and wrapping it in an outer object
({ "user": { ... } }) adds a layer of nesting that consumers have to unwrap for no real benefit
in the single-resource case.
The collection response with pagination
Lists are different — they need room for metadata, so an envelope earns its keep:
GET /users?page=2&limit=20
{
"data": [
{ "id": 21, "name": "..." },
{ "id": 22, "name": "..." }
],
"pagination": {
"page": 2,
"limit": 20,
"totalItems": 214,
"totalPages": 11
}
}
This shape scales gracefully: today it's just page and limit, tomorrow you might add a
nextCursor for cursor-based pagination, and existing consumers reading data and
pagination are unaffected.
Offset pagination vs. cursor pagination
Page/limit (offset pagination) is simple and lets users jump to an arbitrary page, but it has a real
weakness: if items are added or removed while someone is paging through results, rows can shift between
pages, causing items to be skipped or shown twice. Cursor-based pagination (returning an opaque
nextCursor token that points to "continue after this specific item") avoids that problem and
scales better on very large datasets, at the cost of not being able to jump directly to page 7. Offset
pagination is fine for small, relatively static datasets; cursor pagination is the better choice for large or
frequently changing ones, like an activity feed.
The consistent error response
Every error, regardless of cause, should have the same overall shape:
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "No user found with id 999.",
"status": 404
}
}
Pair this with the correct HTTP status code (404 for not found, 400 for a bad request, 401/403 for auth issues, 500 for a server error) — the JSON body and the status code should always agree, since some client libraries branch on status code alone and never even look at the body.
Validation errors need more detail
A single error code isn't enough when a form submission fails validation on multiple fields at once. A richer shape helps the client show the user exactly what to fix:
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request failed validation.",
"fields": [
{ "field": "email", "message": "Must be a valid email address." },
{ "field": "age", "message": "Must be 18 or older." }
]
}
}
Partial success: the bulk operation problem
What happens when a client submits 50 items and 47 succeed but 3 fail? Silently succeeding (hiding the failures) or silently failing the whole batch (rejecting 47 good items over 3 bad ones) are both bad options. A response that reports per-item results lets the client know exactly what happened and retry only what failed:
{
"succeeded": 47,
"failed": 3,
"errors": [
{ "index": 12, "code": "DUPLICATE", "message": "Item already exists." },
{ "index": 33, "code": "INVALID_SKU", "message": "SKU not recognized." },
{ "index": 44, "code": "OUT_OF_STOCK", "message": "No inventory available." }
]
}
Empty results are not errors
A search that finds nothing is a perfectly normal outcome, not a failure. Return 200 OK with an
empty data array, not a 404. Reserve 404 for "the specific resource you asked for by ID doesn't
exist," not "your search returned zero matches."
The pattern underneath all of these
Every one of these examples follows the same underlying principle: a response shape should be predictable enough that a client can write generic handling code once, instead of writing special-case logic for every individual endpoint. That consistency is worth actively designing for, not something that happens by accident as an API grows.