JSON itself is simple. JSON APIs that are pleasant to use, evolve well, and don't generate support tickets — those take taste. Here are twelve rules that consistently distinguish APIs developers like to integrate with from ones they curse.
1. Pick one casing and stick with it
The two reasonable choices are camelCase (matches JavaScript) and snake_case (matches Python, most SQL). Both are fine. What's not fine is mixing them in the same response. {"userId": 1, "user_name": "alice"} tells a consumer your API was built by multiple people who didn't talk to each other.
If you have public consumers in many languages, snake_case is slightly friendlier because JavaScript devs have to convert one way or the other anyway, and Python/Ruby/Rust devs find camelCase more annoying than the reverse.
2. Use plural for collections, singular for items
GET /users returns a list; GET /users/123 returns one. The collection is wrapped in a top-level array (or paginated object), the item is the bare object. This matches what your URL says.
3. Wrap collections in a paginated envelope
Returning a raw array ([...users]) seems clean — until you need to add pagination and have a breaking change on your hands. Always start with:
{
"data": [...],
"page": { "cursor": "abc", "has_more": true }
}
Even when you have ten users today. The day you have ten thousand, you'll thank yourself.
4. Use ISO 8601 for dates, in UTC
Always. "2026-01-15T10:30:00Z". Not Unix timestamps (they're hard to read in logs), not human-formatted dates (locales differ), not "2 hours ago" (compute on the client). ISO 8601 sorts lexically, which is a useful side benefit.
5. Be explicit about null
Decide whether a missing field and a null field mean the same thing in your API, and document it. Most APIs are well served by always including all fields, with null for absent values. That way clients don't have to guard against missing keys.
The opposite convention — omit nulls — saves bytes but pushes a tax onto every consumer.
6. Don't put types in keys
{"name_str": "Alice", "age_int": 30} is a tell that someone is reimplementing static typing in their key names. Don't. Use JSON Schema or OpenAPI to describe types out-of-band.
7. Send big integers as strings
JavaScript numbers are 64-bit floats. Anything above 2^53 (about 9 quadrillion) loses precision. If your IDs or amounts can exceed that — and database IDs from Twitter, Stripe, and many high-volume systems do — send them as strings: {"id": "9007199254740993"}. Document it. Validate it.
8. Have a consistent error format
Pick one envelope and use it everywhere. RFC 7807 (Problem Details for HTTP) is a reasonable default:
{
"type": "https://example.com/errors/insufficient-funds",
"title": "Insufficient funds",
"status": 403,
"detail": "Account balance $5 is less than withdrawal $20.",
"instance": "/accounts/abc"
}
Don't return HTML when the request was JSON. Don't return different shapes for different errors. Don't return 200 OK with {"error": "..."} — use the HTTP status code as the primary signal.
9. Version the URL, not the payload
Put /v1/ in the URL path. It's the convention people expect, it's cacheable, and clients pin to it explicitly. Versioning via Accept headers is technically purer and practically annoying.
10. Cursor pagination beats offset for any list that changes
?offset=200 breaks if records are added or deleted while a client pages through. ?cursor=abc is opaque — the server can encode any stable position into it. Use offset only for stable, sortable, finite lists.
11. Don't echo request data unnecessarily
POST /users with a request body, server creates the user, server returns the created user. Don't wrap that response in {"request": {...}, "response": {...}}. The client already has the request — they sent it.
12. Use enums, not strings of strings
"status": "active" is good. The set of allowed values should be documented in your schema, ideally as "enum": ["active", "suspended", "deleted"]. Clients can validate at the edge, and new values are a documented change.
Bonus: things to actively avoid
Don't use JSON for everything. Streaming logs? Use newline-delimited JSON (NDJSON) — one object per line. Time-series? Use a column-oriented format. JSON shines at small, structured payloads.
Don't put HTML in JSON. If you have to deliver HTML, deliver it as HTML, not as a string field in a JSON envelope. Escaping nightmares lie that way.
Don't deeply nest unless the structure is intrinsically nested. data.payload.body.user.profile.name tells a consumer you didn't think about ergonomics. Flatten where possible.
Wrap-up
The common thread across all twelve rules: think about the consumer. Every choice you make in the response shape pushes work either onto you (writing the format) or onto every client (parsing around it). The former scales; the latter doesn't.