Payload Conventions

Payload Conventions and Envelopes

An envelope is a wrapper around the resource: {"data": {...}} instead of {...}. It costs eight bytes and buys a place for everything that is about the response rather than part of it — page counts, links, warnings — without colliding with a field of the resource. Return a bare object and the first time you need a nextCursor you have two bad options: a top-level key that could one day be a book property, or a break for every client. Use the envelope for single resources and collections alike, keep the same keys everywhere, and give errors their own shape, the {"error": {"code", "message", "details"}} object of Error Middleware — never an error under a 200.

Three conventions inside the resource save arguments later. Use camelCase keys, because every consumer in this stack is JavaScript. Send timestamps as ISO 8601 in UTC with the Z suffix, which new Date(s) parses and toISOString produces. And send money as integer minor units (priceCents: 1299), never a float. JSON numbers have a related trap:

Output of 60
> GET /api/v1/books/bk_033
< 200 OK
{"data":{"id":"bk_033","title":"The Mirror & the Light","author":"Hilary Mantel","year":2020,
 "genre":"historical","rating":4,"inStock":false,"updatedAt":"2026-08-02T09:30:00.000Z"}}

The rating is stored as 4.0 and arrives as 4, because JSON.stringify writes the shortest form that round-trips. A UI formatting with String(rating) then prints "4" beside "4.2", and a schema inferred from that sample says type: integer where the next says type: number — so declare it instead. The same care applies past 2^53, where JSON.parse turns 9007199254740993 into 9007199254740992 without a word, so large integers travel as strings. res.json() is JSON.stringify, so a BigInt throws outright.