When you design a JSON endpoint that returns a list of resources, what is a response envelope with `data` and `meta` sections, and why might you prefer it over returning a bare JSON array as the top-level body?
answer
- bare array = closed shape, no extension point
- data / meta / links
- envelope vs Link header — both legitimate
- additive meta keys = non-breaking
- one shape for every collection
basics
~20 sAn envelope wraps the items in an object: data holds the array, meta holds paging info (page size, cursors, counts), and links holds next/prev URLs. A bare array leaves no room to add that without breaking clients.
solid answer
~50 sA bare top-level array (`[{...},{...}]`) is a closed shape: there is nowhere to put paging state, and any later addition is a breaking change for every parser. An envelope makes the body an object: ```json { "data": [...], "meta": { "page_size": 50, "next_cursor": "c3Y9" }, "links": { "next": "..." } } ``` `data` is the payload, `meta` is machine-readable paging/state, `links` are ready-made URLs the client follows. Benefits: extensible (new meta keys are additive), uniform across all list endpoints so one client helper handles every collection, and it avoids the historical JSON-array-hijacking concern for browser-served JSON. Costs: one extra level of nesting, and single-resource responses should follow the same convention or you get two shapes. The alternative is keeping the body a pure array and putting paging in the RFC 8288 `Link` header — also valid; the mistake is doing neither and inventing ad-hoc `total`/`page` fields per endpoint.
code
json · 15 lines{
"data": [
{ "id": "o_1", "total": 1999 },
{ "id": "o_2", "total": 450 }
],
"meta": {
"page_size": 2,
"has_more": true,
"next_cursor": "b3ffMg"
},
"links": {
"self": "https://api.example.com/v1/orders?limit=2",
"next": "https://api.example.com/v1/orders?limit=2&starting_after=o_2"
}
}go deeper
Know what the envelope looks like and that a bare array leaves no room for paging metadata. Name data, meta, links.
Compare the envelope with the Link header approach, and explain that meta keys are additive while a top-level type change is breaking.
Talk about consistency across endpoints, generating body links and the Link header from one source, and what belongs in meta versus what costs too much to compute.
Frame it as an API-wide standard: one collection contract, client SDK generation, how the envelope survives versioning and gateway rewriting, and how you migrate services that already shipped a different shape.
## The problem A collection endpoint has to return two different kinds of information: the items themselves, and information *about* the result set — how big a page was requested, whether more pages exist, how to get the next one. A bare JSON array can only carry the first. ```http GET /v1/orders HTTP/1.1 HTTP/1.1 200 OK Content-Type: application/json [ {"id":"o_1"}, {"id":"o_2"} ] ``` This is fine until the day you need to say "there is a next page". The top-level type is an array; adding a sibling key is impossible, and changing the top-level type to an object breaks every client that does `for (const o of body)`. ## The envelope An envelope makes the top-level body an object with well-known slots: - **`data`** — the array of resources (or the single resource, for item endpoints). Sometimes named `items`, `results`, or `records`; the name matters less than picking one and using it everywhere. - **`meta`** — machine-readable metadata about the result set: `page_size`, `next_cursor`, `has_more`, sometimes `total_count`. - **`links`** — hypermedia: fully-formed URLs for `self`, `next`, `prev`, `first`, `last`. Giving the client a URL rather than a cursor value means the client never has to know how to build the query string. JSON:API standardizes exactly this trio (`data` / `meta` / `links`); Stripe uses a flatter variant (`object: "list"`, `data`, `has_more`, `url`); Google's AIP style returns `{ items..., nextPageToken }`. All three are envelopes. ## Why an envelope helps **Extensibility.** New keys in `meta` are additive and non-breaking. A bare array has no extension point at all — the only escape hatch is response headers. **Uniformity.** If every list endpoint returns the same shape, one client-side function can iterate any collection: read `data`, follow `links.next`, repeat. Ad-hoc per-endpoint shapes (`{orders: [...], totalOrders: 12}`) force bespoke code per endpoint and are the most common real-world defect here. **Errors and partial results.** An object body has somewhere to put warnings, deprecation notices, or a partial-failure list. An array does not. **Security folklore.** Top-level JSON arrays were once exploitable in browsers via `<script>` tag inclusion combined with `Array` constructor overrides (JSON hijacking). Modern browsers closed this, and it only ever applied to cookie-authenticated GETs, but it is why several style guides still forbid bare arrays. Do not present it as the main reason — the extensibility argument is the honest one. ## The cost, and the alternative The envelope costs a level of nesting on every access (`body.data[0].id`) and creates a decision for single-resource endpoints: either wrap them too (consistent, slightly silly) or don't (two shapes to learn). Wrapping consistently is the usual call. The legitimate alternative is the **transport-level** answer: keep the body a pure array and put paging in the RFC 8288 `Link` header, as GitHub does. That keeps bodies clean and lets generic HTTP tooling see the paging, but it hides paging state from tools that only log bodies, from browser fetch code that must explicitly read headers, and it is awkward in gateways or client SDKs that drop headers. Some APIs do both — `Link` header *and* envelope `links` — which is redundant but harmless. ## What interviewers listen for A candidate who says "envelope, because I'll need `next` later and I can't add it to an array" has the point. A candidate who mentions that `Link` headers are an equally valid, standards-based answer, and that the real sin is inconsistency across endpoints, is answering at senior depth. The wrong answer is inventing a fresh shape per endpoint, or adding an unbounded `total` to every response without thinking about what counting costs.
- Should single-resource responses use the same envelope?Usually yes, for consistency: one deserializer and one mental model for the whole API, and it leaves room for per-response `meta` such as deprecation warnings. The cost is a pointless-looking wrapper around a single object. Whichever way you go, apply it uniformly — mixing wrapped collections with unwrapped items is the shape clients complain about most.
- If you already return a `Link` header, is putting `links` in the body redundant?Strictly yes, and duplicating them means two places to keep correct. It is a deliberate redundancy some APIs accept because browser and SDK clients find body fields easier to reach than headers. If you duplicate, generate both from one source in the server so they can never disagree.
A bare array is a padded envelope with only the letter inside; a response envelope has the letter plus the address label and the 'page 1 of 3' note on the outside.
saying these in an interview costs you the question
- Claiming a bare top-level array is forbidden for security reasons, without knowing the JSON-hijacking issue is obsolete in modern browsers
- Inventing a different envelope per endpoint (`orders`, `totalOrders`) instead of one uniform shape
- Putting the item count of the current page in `meta` and calling it the total
- Assuming an envelope is required — not knowing that RFC 8288 `Link` headers are an equally standard answer
- Adding paging fields alongside the resource fields at the top level, so the payload and the metadata are indistinguishable