JSON API Design Best Practices: Naming, Dates, Money, Errors and Pagination
API design 11 min readUpdated October 12, 2026
A JSON API is a promise to other developers about the shape of your data. Small decisions made early, such as how you write dates or whether a missing value is null, are very hard to change once clients depend on them. These practices come from the mistakes teams repeat most often. Use them as a checklist when you design a new API or review an existing one.
In this guide
- 1. Be consistent about names
- 2. Use the right type for each value
- 3. Decide what null and "missing" mean
- 4. Return an object at the top level
- 5. Paginate every list
- 6. Make errors consistent and machine-readable
- 7. Change the API without breaking clients
- 8. Protect your server from bad JSON
- 9. Document with real examples and a schema
- Quick checklist
- Frequently asked questions
1. Be consistent about names
- Pick one key style, either
camelCase(createdAt) orsnake_case(created_at), and use it everywhere. Mixing them is the most common and most annoying inconsistency. - Use the same name for the same concept across endpoints. If a user's id is
userIdin one response, it should not beuidoruser_idin another. - Use plural names for arrays (
orders) and singular names for single objects (order). - Do not use data as keys.
{"2026-10-12": 5}is hard to document and validate; prefer a list of objects such as[{"date": "2026-10-12", "count": 5}].
2. Use the right type for each value
| Kind of data | Recommended | Avoid |
|---|---|---|
| Dates and times | ISO 8601 / RFC 3339 strings in UTC, such as "2026-10-12T09:30:00Z" | Unix timestamps with no stated unit, or local formats like "12/10/2026" that mean different things in different countries |
| Money | Integer minor units (24950 paise or cents) plus a currency code, or a decimal string like "249.50" | Floating-point numbers, which cannot represent most decimal fractions exactly |
| Identifiers | Strings, particularly if they are large or could have leading zeros | Numbers beyond 9,007,199,254,740,991, which JavaScript clients silently round |
| Flags | Real booleans: true and false | "yes", "Y", 1 or "true" as a string |
| Fixed choices | Lowercase string constants like "shipped" | Magic numbers such as 3 |
| Units | A clear field name (weightKg) or a separate unit field | A bare number whose unit has to be guessed |
Keep each field to one type. A field that is sometimes a string and sometimes an object, or sometimes a single object and sometimes an array, forces every client to write defensive code and eventually breaks one of them.
3. Decide what null and "missing" mean
There are three states for an optional value: it has a value, it is explicitly empty, or it is not mentioned. Choose a convention and document it:
- In responses, either always include the key (using
nullfor no value) or omit absent keys. Always including keys gives clients a predictable shape and is easier to type-check. - In update requests, treat a missing key as "leave unchanged" and
nullas "clear this field". This distinction is what makes partial updates (PATCH) work. - Return empty arrays as
[], notnull, so clients can loop without checking.
4. Return an object at the top level
A response that is a bare array cannot grow. If you later need to add a total count or a link to the next page, you would have to break every client. Wrapping the list in an object leaves room for it.
{
"data": [
{ "id": "ord_1001", "status": "shipped", "total": { "amount": 24950, "currency": "INR" } },
{ "id": "ord_1002", "status": "pending", "total": { "amount": 8000, "currency": "INR" } }
],
"page": {
"nextCursor": "eyJpZCI6Im9yZF8xMDAyIn0",
"hasMore": true
}
}5. Paginate every list
A list endpoint that returns everything works on day one and fails when the data grows. Always put a limit on the page size, and prefer cursor-based pagination for data that changes: the client sends back the cursor from the previous page and gets the next items, with no skipped or repeated records when new data arrives. Offset pagination (?page=3) is simpler and fine for small, stable data sets, but it gets slow for deep pages and unreliable under concurrent writes.
6. Make errors consistent and machine-readable
Use proper HTTP status codes (400 for invalid input, 401 and 403 for authentication and permission problems, 404 for not found, 409 for conflicts, 429 for rate limits) and give every error the same body shape. RFC 9457, "Problem Details for HTTP APIs", defines a standard format with the media type application/problem+json.
{
"type": "https://api.example.com/problems/validation-error",
"title": "Your request is not valid",
"status": 400,
"detail": "2 fields failed validation.",
"errors": [
{ "field": "email", "message": "must be a valid email address" },
{ "field": "age", "message": "must be at least 18" }
]
}Never leak stack traces, SQL or internal hostnames in error bodies.
7. Change the API without breaking clients
- Safe changes: adding a new optional field, a new endpoint, or a new value to a response. Tell clients in your documentation to ignore fields they do not recognize.
- Breaking changes: removing or renaming a field, changing its type or meaning, making an optional input required, or adding a value to an enum that clients switch on without a default case.
- Release breaking changes as a new version (in the path, such as
/v2/orders, or in a header), and keep the old one running for a published period. - Never reuse a field name for something different.
8. Protect your server from bad JSON
- Limit the size of request bodies, and the nesting depth that the parser accepts, to avoid memory and stack problems.
- Validate the shape and values of every incoming document, ideally against a JSON Schema, and reject unknown fields if strictness is appropriate.
- Send
Content-Type: application/json; charset=utf-8and expect it on requests. - Build JSON with a serializer, never by joining strings, so quoting and escaping are always correct.
- Do not return more than the caller is allowed to see. Filter fields on the server rather than trusting clients to ignore them.
9. Document with real examples and a schema
Publish a machine-readable description of your API, such as an OpenAPI document using JSON Schema for the bodies, together with complete example requests and responses for the normal case, empty results and common errors. Examples that were generated from the real API stay accurate, and you can use them as test fixtures.
Quick checklist
- One naming style, used consistently.
- Dates in ISO 8601 UTC; money as integers or decimal strings; large ids as strings.
- A documented rule for null versus missing; empty lists are
[]. - Object at the top level; every list paginated.
- One standard error format with correct status codes.
- Additive changes only within a version.
- Input size, depth and shape validated on the server.
- Schema and real examples published.
Frequently asked questions
Should I use camelCase or snake_case in JSON?
Either is fine. JavaScript-heavy ecosystems usually use camelCase and Python- and Ruby-heavy ones often use snake_case. What matters is being consistent across the whole API, and matching what your main clients expect.
Is it OK to put a version in the JSON body?
It is better to version in the URL or a header, because the version then applies before the body is read. A schema version field inside documents is useful for stored data, where a record may be many years old.
Should dates include a time zone?
Yes. Send UTC with the Z suffix, or an explicit offset, and let clients convert to local time for display. A date-and-time string without a zone is ambiguous.
What is JSON:API, and is it the same thing?
JSON:API is a specific specification that defines a fixed document structure for resources, relationships and errors. It is one way to apply many of these ideas. This guide describes general practices that apply whether or not you adopt that specification.