1. Naming Conventions: Pick One, Apply Everywhere
The single most visible source of inconsistency in API JSON is mixed naming styles. JSON itself (per RFC 8259) imposes no requirement, but your API should. The three main options:
camelCase— dominant in JavaScript/TypeScript ecosystems. Recommended for public APIs.snake_case— common in Python, Ruby, and database-aligned APIs.PascalCase— rarely used in JSON; avoid for general APIs.
{
"user_id": 42,
"firstName": "Alice",
"LastName": "Chen",
"email_address": "a@ex.com"
}{
"userId": 42,
"firstName": "Alice",
"lastName": "Chen",
"emailAddress": "a@ex.com"
}2. Dates and Times: Always ISO 8601, Always UTC
Timestamp inconsistency is the #1 cause of timezone bugs. The only safe choice is ISO 8601 format with explicit UTC offset:
{
"createdAt": "06/07/2026",
"updatedAt": 1749254400,
"expiresAt": "Jun 7 2026 10:30 AM"
}{
"createdAt": "2026-06-07T10:30:00Z",
"updatedAt": "2026-06-07T14:22:11Z",
"expiresAt": "2026-12-31T23:59:59Z"
}The Z suffix explicitly means UTC. Consumers in any language/timezone can parse it unambiguously. Unix timestamps in the JSON body make debugging painful — save them for headers if needed at all.
3. Null vs Missing Keys: Have a Clear Policy
A missing key and a null value are semantically different. Be consistent:
- Null means "the field exists but has no value" — e.g., a user with no middle name:
"middleName": null - Missing key means "this field does not apply to this resource" — e.g., a guest checkout order has no
userIdkey at all
Never use empty strings "" to represent "no value" for non-string types. Never use 0 or -1 as sentinel values for missing IDs. Use null.
When evolving your API, only add new keys. Never remove or rename existing keys in a minor version — consumers depend on them. Clients must be written to ignore unknown keys (this is standard JSON parsing behaviour). This gives you forward compatibility for free.
4. Consistent Error Format
Nothing makes an API harder to integrate than inconsistent error responses. Define one error envelope and use it everywhere:
{
"error": {
"code": "VALIDATION_FAILED", // machine-readable code
"message": "Email is required", // human-readable description
"field": "email", // field that caused the error
"requestId": "req_8a3f2b1c", // for log correlation
"docsUrl": "https://api.example.com/errors/VALIDATION_FAILED"
}
}Key principles: code is a stable, uppercase string — never an integer that might change. message is human-readable but not user-displayable (localise separately). requestId lets developers cross-reference your server logs instantly.
5. Pagination: Cursor Over Offset
Offset-based pagination (?page=3&limit=20) is easy to implement but breaks on real data — items inserted between pages cause duplicates or skips. Cursor-based pagination is better for production:
{
"data": [ /* ... array of items ... */ ],
"pagination": {
"limit": 20,
"total": 847,
"nextCursor": "eyJpZCI6MTIzfQ", // opaque base64 cursor
"hasMore": true
}
}Always wrap list responses in an object with a data array. Never return a bare array at the top level — it prevents you from adding metadata (pagination, warnings, totals) later without a breaking change.
6. IDs: Strings, Not Integers
JavaScript's Number type safely represents integers only up to 2⁵³−1 (9,007,199,254,740,991). Many databases use 64-bit integers that exceed this. Return all IDs as strings:
{
"id": 9007199254740993
// JS parses as 9007199254740992
// Silent data corruption!
}{
"id": "9007199254740993"
// Exact. No precision loss.
// Works in every language.
}7. Boolean Traps to Avoid
Booleans seem simple but cause surprising pain:
- Don't encode boolean states as strings:
"status": "true"is wrong. Use"active": true. - Don't use
0/1for booleans in JSON — that's a C pattern, not JSON. - Prefer positive framing:
isActiveinstead ofisNotDeleted. Double negatives in code are bugs waiting to happen.
- One naming convention (prefer camelCase) — enforced everywhere
- All timestamps as ISO 8601 with Z (UTC)
- Clear null vs missing key semantics documented
- Additive-only changes to existing payloads
- One consistent error envelope across all endpoints
- List responses wrapped in
{ "data": [] }envelope - IDs returned as strings, not integers
- Booleans as
true/false, never"true"or1