← Blog · Written by the JSONNeat maintainer · Published

REST API JSON Best Practices: Field Names, Errors & Pagination (2026)

Designing an API that lives for ten years is mostly about getting the JSON shape right at the start. Field names, error formats, pagination semantics, type choices — these decisions cement themselves into clients' code the moment your first integration ships. Changing them later means coordinating with every consumer or breaking some of them. The goal of this guide is to help you make the decisions you can live with.

These aren't rules from a spec. They're patterns that have stuck across thousands of public APIs and survived contact with reality. Stripe, GitHub, Twilio, Slack and OpenAI all converge on roughly the same conventions for a reason: they reduce the cognitive load on clients and the support load on the team running the API.

Naming conventions

Pick camelCase or snake_case and use it everywhere.

// camelCase — the JavaScript-friendly default
{
  "userId": 42,
  "createdAt": "2026-04-22T10:30:00Z",
  "isActive": true
}

// snake_case — common in Python/Ruby-backed APIs
{
  "user_id": 42,
  "created_at": "2026-04-22T10:30:00Z",
  "is_active": true
}

camelCase is slightly more common in JSON ecosystems and matches JavaScript variables naturally. snake_case is fine, especially if your backend is Python or Ruby. What matters is consistency: never mix createdAt and updated_at in the same response. Pick one in your style guide, enforce it in code review, and stick with it.

A few naming rules that survive every team's bikeshed:

  • Boolean fields start with is or has. isActive, hasPaidSubscription. Reading the field name should make the meaning obvious without consulting docs.
  • Datetime fields end in At. createdAt, updatedAt, deletedAt. Date-only fields (no time component) end in On or Date: birthDate, expirationOn.
  • Foreign-key IDs are <entity>Id. userId, accountId, organizationId. Easier to scan, easier to grep.
  • Avoid abbreviations except universally understood ones. url is fine. addr for address is not.

Use ISO 8601 for every date and time

{
  "createdAt": "2026-04-22T10:30:00Z",
  "expirationDate": "2026-12-31"
}

Don't invent custom date formats. Don't use Unix timestamps unless you have a measurable reason — they look like just numbers in a payload, which makes debugging harder and confuses humans reading logs. ISO 8601 is unambiguous, sortable as strings, and parseable by virtually every language's standard library.

For timestamps, always include a timezone. Z (UTC) is the safe default. +00:00 is equivalent. Without a timezone, clients have to guess, and they will guess wrong.

For dates without a time (a birthday, an expiration), use just YYYY-MM-DD. Don't pad with a fake time component — that introduces timezone ambiguity ("does 1990-01-01T00:00:00Z mean "the Jan 1 1990 calendar day" or "Dec 31 1989 in Pacific Time?").

Use strings for large IDs

JSON numbers are IEEE-754 doubles. Integers above 2^53 - 1 (about 9 quadrillion) cannot be represented exactly. Database row IDs from BIGINT columns, Snowflake IDs, Twitter status IDs and many UUID-as-integer formats exceed this:

// Wrong — clients in some languages will silently lose precision
{ "id": 9007199254740993 }

// Right — strings preserve every digit
{ "id": "9007199254740993" }

This bug is famously hard to notice because the loss is silent and the symptom (records returning the wrong neighbour ID) is far from the cause. Twitter learned this lesson publicly in 2010 when their status IDs grew past 2^53 and JavaScript clients started returning the wrong tweet.

The defensive rule: if an ID *could* exceed 2^53 - 1 within ten years of growth, make it a string from day one. The cost of doing so is one character of quotation in the JSON.

A consistent error envelope

Every error response should have the same shape so clients can write one handler instead of N:

{
  "error": {
    "code": "user_not_found",
    "message": "No user exists with id 42.",
    "details": { "id": 42 },
    "requestId": "req_abc123",
    "documentationUrl": "https://docs.example.com/errors/user_not_found"
  }
}

Fields and what they're for:

  • code — machine-readable, stable across releases. Clients branch on this. Lower-case snake_case is the convention.
  • message — human-readable, may change over releases. Suitable for showing to end users or logging.
  • requestId — opaque identifier you also include in your server logs. When a user emails support, they quote this and you can find the exact request in seconds.
  • details — structured context. For validation errors, an array of field-level problems. For rate-limit errors, the retry-after time. Schema varies by error code.
  • documentationUrl — optional link to a docs page explaining the error. Helps with debugging without burdening the error message.

Use HTTP status codes for the category (400 = client error, 500 = server error, 404 = not found) and the code field for the specific error within the category. Don't put long enums in HTTP status codes — that's not what they're for.

Pagination patterns

Two patterns dominate. Pick one per endpoint and document it.

Cursor-based (recommended for collections that grow over time or where new records can appear at the head):

{
  "data": [ ... ],
  "pageInfo": {
    "endCursor": "eyJpZCI6MTAwfQ==",
    "hasNextPage": true
  }
}

The cursor is opaque (typically a base64-encoded reference to the last record). The client sends it back in the next request as a query parameter: ?after=eyJpZCI6MTAwfQ==. Two huge advantages: it scales to any collection size, and it's stable under inserts (new records at the head don't shift existing pages).

Offset-based (simpler, fine for small collections, dies on large ones):

{
  "data": [ ... ],
  "page": 3,
  "pageSize": 50,
  "total": 12345
}

The client requests ?page=4&pageSize=50. Easy to implement, easy to reason about, supports "jump to page N" UIs. But OFFSET 5000 on a database query gets slower as the offset grows, and inserts can cause records to appear or disappear from already-visited pages.

Rule of thumb: if you expect a collection to exceed 10,000 items or change frequently, use cursor pagination. Below that, offset is fine.

Versioning

Three strategies, each with its own trade-offs:

  1. URL versioning/v1/users, /v2/users. The most explicit, the most discoverable, and the easiest to reason about. Stripe uses date-based versions in URLs (/v1/charges plus an API-Version header for finer granularity).
  1. Header versioningAccept: application/vnd.example.v2+json. Keeps URLs clean, allows version per resource. Harder to test in a browser; less obvious to developers reading logs.
  1. No versioning, evolve carefully. Only add fields, never remove or change semantics. Works for small private APIs but is fragile — one accidental breaking change ruins it.

URL versioning is the safest default for public APIs. Internal APIs with one or two consumers can sometimes get away with strategy 3 if everyone is disciplined.

Field stability — the rules every client should be able to trust

A few rules consumers can rely on save them from breakage:

  • Adding a new field is non-breaking. Clients should ignore unknown fields. (Document this expectation explicitly.)
  • Removing a field is breaking. Bump the major version.
  • Changing a field's type is breaking. age: 42 to age: "42" is a breaking change.
  • Changing a field's semantics is breaking. Often silently. Avoid this aggressively — if "score" used to mean "0-100" and now means "0-1", every client breaks.
  • Reordering fields is non-breaking. JSON objects are unordered by spec.

Document these rules in your API guide and enforce them in your release process. A schema-diff test in CI catches accidental breakages before they ship.

Null vs missing — be intentional

These two are not the same:

// "phoneNumber" is null — we know there is no phone number
{ "name": "Ada", "phoneNumber": null }

// "phoneNumber" is missing — we don't have or don't return that data
{ "name": "Ada" }

Pick a convention per field and document it. Common patterns:

  • Always include the field, use null for absence. Easier for clients with strict type systems — phoneNumber: string | null is a clean type.
  • Omit when absent, never null. Smaller payloads, but clients have to check for presence.

Whichever you pick, be consistent within a resource. Mixing the two within the same object is the worst of both worlds — clients have to handle both cases.

Numbers: integer vs decimal vs string

For currency, use either integer-cents ({"amountCents": 1999} for $19.99) or a string-decimal ({"amount": "19.99"}). Never use floats — 0.1 + 0.2 !== 0.3 and that error compounds. Both Stripe (cents) and PayPal (string decimals) handle this carefully.

For measurements with units, include the unit in the field name or the value: {"durationMs": 1500} or {"duration": "1.5s"}. Don't make clients guess.

For percentages, decide whether {"completion": 0.42} (fraction) or {"completion": 42} (percent) and use the same convention everywhere.

Don't reinvent every wheel

If you find yourself designing from scratch, look at the giants first:

  • Stripe's API is the de facto reference. Read the docs cover-to-cover before designing your own — the patterns generalise to almost any domain.
  • JSON:API is a fully-specified format if you want to delegate the structure decisions entirely.
  • GraphQL is a different model entirely, but worth understanding before committing to REST. For APIs with many resource types and complex client query patterns, it can be a better fit.

Test your responses

Whatever you decide, test it. Three layers of tests catch most regressions:

  1. JSON Schema validation in your API tests — for every endpoint, every response shape. See our JSON Schema tutorial.
  2. Snapshot tests — record canonical responses for representative inputs and diff on every change. Forces deliberate decisions about every breaking change.
  3. Type generation in client SDKs — when types are generated from your schema, breaking changes break the client build, not production traffic.

Format and validate your sample responses with JSONNeat and the JSON validator during development to catch shape regressions before clients do. A five-minute paste-and-check at design time saves hours of customer escalations later.

The summary

Good JSON API design is the discipline of making opinions explicit. Pick a casing, a date format, an error envelope, a pagination model, a versioning strategy — write them down, enforce them, and stick with them. Most of your future API headaches come from inconsistency, not from picking a slightly worse-than-optimal convention. Consistency compounds; cleverness erodes.

When in doubt, copy Stripe. They've already made every decision you're about to make, and most of them are right.


Written by the maintainer of JSONNeat. Questions or corrections? Email [email protected].

Related tools: JSON formatter, validator, minifier.