JSON and REST grew up together. Although REST itself doesn't mandate any particular format, the overwhelming majority of "REST-style" APIs in the wild use JSON. Over twenty years of practice, a set of conventions has emerged. Here are the patterns most APIs follow and the pitfalls worth avoiding.
Resource representation
A REST resource is typically represented as a JSON object. The object contains the resource's fields plus, optionally, metadata like identifiers and timestamps:
GET /users/42
{
"id": 42,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2024-03-15T10:30:00Z"
}
Collections are JSON arrays of these objects, or — more commonly — an envelope object containing the array plus pagination metadata.
Naming conventions
The two dominant conventions for JSON field names are snake_case (created_at, user_id) and camelCase (createdAt, userId). Neither is universally preferred. Choose one and stick with it across your entire API. Mixing styles within a single API is the worst option — it forces every consumer to remember which fields use which convention.
Snake_case tends to dominate Python and Ruby ecosystems; camelCase dominates JavaScript and Java. Google's APIs use camelCase. GitHub's API uses snake_case. The Python tooling will be slightly happier with snake_case (no conversion needed); JavaScript tooling slightly happier with camelCase.
Pagination
Three patterns are common, in order of popularity:
Cursor-based. The server returns a cursor (opaque string) that the client passes to fetch the next page. Best for streams and large datasets:
{
"data": [...],
"next_cursor": "eyJpZCI6MTAwfQ=="
}
Offset-based. The client requests ?offset=100&limit=20. Simple but expensive for the database on deep pages, and unstable when items are inserted or deleted between requests:
{
"data": [...],
"total": 1842,
"offset": 100,
"limit": 20
}
Page-based. The client requests ?page=5. Easiest for users to understand; same downsides as offset-based.
For new APIs, prefer cursor-based pagination. Stripe, Twitter, and GitHub all use cursors for their high-volume endpoints.
Error formats
An error response should be a JSON object that's easy to parse and useful to display. RFC 7807 (Problem Details for HTTP APIs) defines a standard format:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json
{
"type": "https://example.com/errors/validation",
"title": "Validation failed",
"status": 422,
"detail": "The email field is not a valid email address.",
"errors": [
{"field": "email", "message": "Invalid format"}
]
}
Most APIs don't use RFC 7807 strictly but follow its spirit: a top-level error message plus a list of field-level errors when applicable. Critically, the HTTP status code should match the error type — 400 for malformed requests, 401 for missing auth, 403 for forbidden, 404 for not found, 422 for validation failures, 5xx for server errors.
Dates and times
Use ISO 8601 strings in UTC: "2026-05-13T14:30:00Z". Don't use Unix timestamps as numbers, don't use locale-specific formats, don't omit timezone information. ISO 8601 is unambiguous, sorts lexically the same as chronologically, and is parsed natively by every language's standard library.
IDs and types
For resource IDs, you have two reasonable choices: integers (auto-incremented database IDs) or strings (UUIDs, slugs, or composite IDs). Both work; mixing them within an API is confusing.
If you choose integers, watch out for the JavaScript Number.MAX_SAFE_INTEGER limit (2^53 − 1). Once you have more than 9 quadrillion records, this matters; before then, it doesn't. If you're worried about it sooner, return IDs as strings.
Null vs missing keys
An object's field can be null (present, value is null) or missing (key not in the object at all). These are different. Most APIs treat them equivalently for reads, but for writes they often differ: missing field = "don't change this", explicit null = "set this to null".
Document the distinction clearly. PATCH endpoints especially benefit from this — it's how you let clients clear a field without touching others.
HATEOAS and hypermedia
The "true REST" purists argue that responses should include links to related resources, so a client can navigate the API without hardcoding URLs:
{
"id": 42,
"name": "Alice",
"_links": {
"self": "/users/42",
"orders": "/users/42/orders"
}
}
In practice, almost no public API does this rigorously, because clients almost always hardcode URLs anyway. Most modern APIs lean toward "RESTful enough" — clean URLs, sensible verbs, JSON payloads — without committing to full HATEOAS.
Versioning
Three approaches dominate. URL versioning (/v1/users) is the simplest and most common. Header versioning (Accept: application/vnd.example.v1+json) is cleaner but harder to test in a browser. No versioning: never break compatibility, only add fields. Most APIs end up doing some mix.
Whichever you choose, add the version number from day one — it's much easier to start versioned than to retrofit versioning after consumers exist.
Content negotiation
Set Content-Type: application/json on requests with bodies. Send Accept: application/json to make explicit what format you want back. The server can return 406 Not Acceptable if it can't satisfy the request.
Don't return JSON with Content-Type: text/plain or text/html. It works for the body but breaks browsers' content sniffing and prevents tooling from parsing automatically.
Pretty-printing vs minified responses
Production APIs should return minified JSON — no extra whitespace. Most clients pretty-print on display anyway, and the bandwidth saving is real for high-traffic endpoints. Some APIs accept a ?pretty=true query parameter that switches to pretty-printed output for human debugging. That's a nice touch.
Either way, the API consumer can always paste a response into our JSON formatter for readability during development.
Common pitfalls to avoid
Wrapping every response in an envelope when it's not needed. {"data": {...actual response...}} looks neat but doubles the keystrokes for every client and rarely earns its complexity. Use envelopes for collections (because you need pagination metadata) but not for single resources.
Returning HTML error pages with 200 OK status. If something went wrong, return the right status code with a JSON error body. Don't return 200 with {"error": "..."} — clients can't tell from headers alone whether the request succeeded.
Inconsistent shapes. If GET /users/42 returns {"id": ..., "name": ...}, then GET /users should return a collection of objects with the same shape — same field names, same casing, same types. Inconsistency between endpoints is the largest source of integration bugs.
For more on JSON in practice, see our JSON best practices guide and JWT vs session cookies for thoughts on auth in REST APIs.