REST API JSON Design: Best Practices
The bytes a REST API returns matter as much as the routes and status codes. Every client that calls your API is written against the shape of its JSON, and reshaping that JSON later breaks them, however minor the change feels internally. This guide covers the response-design decisions that cost little to get right up front and a lot to fix once clients exist.
Pick one envelope shape and stick to it
An envelope is the top-level wrapper every response uses, regardless of endpoint: a known set of keys like `data`, `error`, and `meta` that a client's HTTP layer can handle generically instead of parsing each endpoint differently. Without one, each endpoint with a slightly different shape forces each client to write a slightly different parser.
// Success
{
"data": { "id": 42, "name": "Ada Lovelace" },
"meta": { "requestId": "a1b2c3" }
}
// Failure
{
"data": null,
"error": { "code": "NOT_FOUND", "message": "User 42 does not exist" },
"meta": { "requestId": "a1b2c4" }
}The key names matter less than consistency. Whatever you choose, every endpoint returns it, `data` holds the payload every time, and a client can write one `unwrap(response)` function for the whole API instead of one per resource.
Don't return a bare array (or scalar) at the top level
A `GET /users` that returns a bare JSON array (`[{...}, {...}]`) looks natural, but you can't add response-level metadata later without a breaking change. Once you need a total count, a next-page cursor, or a warning alongside the list, you have to change the top-level type from array to object, which breaks every client calling `response.map(...)` or `response.length` on the parsed body.
Wrap collections in a named field from day one (`{ "users": [...] }` or `{ "data": [...] }`), even when you're sure you'll never need extra metadata. Adding a sibling key to an object stays backward-compatible. Changing an array into an object breaks every client in every language.
Null and omitted send different signals
A field present with value `null` and a field missing from the response both look like 'no value' at a glance, but they mean different things, and clients need to tell them apart. `null` says the field is a known concept for this resource and its current value is nothing (a user with no `middleName`). Omitted says the field doesn't apply, wasn't loaded, or wasn't requested (a sparse fieldset request that asked only for `id` and `name`).
PATCH semantics depend on this distinction. Under JSON Merge Patch (RFC 7396), sending a field as `null` deletes that field from the target, while leaving the field out of the patch body leaves it unchanged. If you conflate the two, or stay vague about which fields can be `null`, clients can't tell 'clear this value' from 'I didn't send an update for this'.
Pagination: cursor vs. offset, and how each looks as JSON
Offset/limit pagination (`?offset=40&limit=20`, or `?page=3&pageSize=20`) is the simplest to implement and reason about, but it has a correctness problem: if someone inserts or deletes rows between page requests, offsets shift under the client, and it skips or duplicates rows. It also gets expensive in SQL at deep offsets, since the database still scans and discards all the skipped rows.
Cursor-based pagination hands the client an opaque token pointing at 'the row after the last one you saw' instead of a numeric position, so results stay stable as the data changes and queries stay cheap at any depth. The trade-off: clients can't jump to an arbitrary page number. They move forward (and, if you support it, backward) from a cursor.
{
"data": [ { "id": 101 }, { "id": 102 } ],
"meta": {
"nextCursor": "eyJpZCI6MTAyfQ==",
"hasMore": true
}
}Default to cursor-based pagination for frequently-changing data (feeds, logs, event streams) or large tables. Offset-based pagination works for small, mostly-static collections where jumping to page N helps (an admin table with a page picker).
Error objects and naming conventions
An error response should carry more than an HTTP status code and a string. Give it a stable, machine-readable `code` a client can branch on (`"VALIDATION_ERROR"`, not a sentence), a human-readable `message` for logs and developers, and for validation failures, a `details` array naming which fields failed and why.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request failed validation",
"details": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be at least 0" }
]
}
}For key naming, camelCase and snake_case both work. Mixing them across endpoints of the same API hurts, because each client then needs field-mapping logic that depends on the endpoint. JavaScript/TypeScript consumers lean toward camelCase since it matches native destructuring; Python and Ruby ecosystems often prefer snake_case. Pick one for the whole API, and apply it to booleans too (`isActive`/`hasAccess`, not a mix of `active` and `has_access`).
Content-Type, charset, and versioning
Two mechanical details round out the contract. Serve `application/json`, the registered media type. Intermediaries won't mangle its charset the way they can with `text/plain`, and JSON is UTF-8 by definition (RFC 8259), so a charset parameter adds nothing. Plan versioning around JSON's compatibility rule: adding an optional field is safe for clients, while removing or renaming a field, changing its type, or narrowing its nullability breaks them. Path versioning (`/v1/`, `/v2/`) with a documented deprecation window (`Sunset` header) gives clients room to migrate.
Common anti-patterns
Stringified JSON inside a JSON field (`"metadata": "{\"plan\":\"pro\"}"` instead of `"metadata": { "plan": "pro" }`) forces every client to parse twice and throws away type safety for no benefit. It usually means the server serialized a database blob column verbatim instead of decoding it before sending the response.
Inconsistent date formats (some fields as Unix timestamps, others as `"08/15/2026"`, others as full ISO 8601) force clients to write per-field date parsing instead of one shared date handler. Standardize on ISO 8601 in UTC with an explicit offset (`"2026-08-15T09:30:00Z"`) everywhere.
Leaking internal database fields means returning ORM artifacts like `password_hash`, internal foreign keys meant for another service, or bookkeeping fields (`__v`, `_id`, `created_by_worker_id`) verbatim in an API response. Map your database model to an explicit response DTO instead of serializing the model. A DTO is the reliable way to stop an internal schema change from turning into a public API change.
FAQ
- Should error responses use the same envelope as success responses?
- Yes. If success responses are `{ "data": ..., "meta": ... }`, error responses should keep that top-level shape with `data: null` and an `error` object populated. A client's response-handling code then checks one thing, the presence of `error`, instead of branching on response shape per status code.
- camelCase or snake_case: does the choice matter?
- The convention itself matters little; consistency matters a lot. Pick one convention for the whole API based on your main consumers' ecosystem, document it, and apply it everywhere, including boolean prefixes and nested object keys. The cost comes from an API where endpoints use different conventions and every client needs endpoint-specific mapping logic.
- Should a new API use cursor or offset pagination?
- Default to cursor-based pagination unless users need to jump to an arbitrary page number in small, mostly-static data. Cursor pagination stays correct when rows change mid-pagination and stays cheap at any depth. Offset pagination degrades in both correctness and performance as the dataset grows or changes.
- Why not serialize my database model directly as the API response?
- Your database schema and your public API contract change for different reasons on different timelines. If you serialize the model directly, any migration, added column, or ORM upgrade can change or leak your public response shape without warning. An explicit response DTO decouples the two, at the cost of one extra mapping step per endpoint.
Try these tools
JSON Validator →
Validate JSON against schemas with detailed error reporting. Generate schemas from JSON.
JSON Formatter →
Format, validate, and minify JSON instantly in your browser, no signup.
JSON Fake Data Generator →
Generate realistic mock JSON, CSV, SQL, and TypeScript datasets dynamically with custom schemas and 18+ field generators.
Related articles
What is JSON? A Complete Beginner's Guide →
JSON (JavaScript Object Notation) is the lightweight data-interchange format behind nearly every API, config file, and NoSQL database. Learn the syntax, the six data types, how parsing behaves in code, and the mistakes everyone makes hand-writing it.
The History of JSON: From 2000 to Industry Standard →
Trace JSON from Douglas Crockford's idea in 2001, through Yahoo's adoption, to becoming the ECMA-404 standard that powers 90% of modern APIs.
The Complete JSON Schema Guide (Draft 7) →
JSON Schema is the standard for describing and validating JSON structure. Learn the core keywords, build a real API schema, compose and reuse schemas with $ref, and run validation in code with Ajv.