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.

Frequently Asked Questions

Not always — a single resource returned directly, flat, is usually cleaner. Envelopes earn their place specifically when you need room for metadata alongside the data, like pagination info on a list endpoint.
Offset pagination (page numbers) is simple and allows jumping to an arbitrary page, but can skip or duplicate items if data changes while paging through it. Cursor pagination (an opaque token pointing to “continue after this item”) avoids that problem and scales better for large datasets, at the cost of not supporting direct page jumps.
No. A 404 means the specific resource requested by identifier doesn't exist. A search or list query that legitimately finds nothing should return 200 OK with an empty results array — the request succeeded, it just found nothing.
Return a response that reports per-item outcomes (which succeeded, which failed and why), rather than treating the whole batch as a single pass/fail unit. This lets the client understand exactly what happened and retry only the failed items.