JSON Web API Design: Naming, Pagination, Errors, and Versioning
Last updated:
A well-designed JSON API is a joy to consume: consistent field names, predictable response shapes, informative errors, and clear pagination. This guide covers the decisions every API designer faces, with concrete examples and the tradeoffs behind each choice.
Field Naming: camelCase vs snake_case
Pick one convention and enforce it everywhere. Mixed casing is the most common API consistency failure.
// camelCase — standard in JavaScript ecosystems (Stripe, GitHub, Twilio)
{
"userId": "usr_123",
"firstName": "Ada",
"lastName": "Lovelace",
"createdAt": "2024-01-15T09:00:00Z",
"isEmailVerified": true,
"billingAddress": {
"streetLine1": "123 Main St",
"postalCode": "10001"
}
}
// snake_case — common in Python/Ruby ecosystems (Slack, Twitter/X)
{
"user_id": "usr_123",
"first_name": "Ada",
"last_name": "Lovelace",
"created_at": "2024-01-15T09:00:00Z",
"is_email_verified": true,
"billing_address": {
"street_line_1": "123 Main St",
"postal_code": "10001"
}
}Common industry choices: camelCase — Stripe, GitHub, Twilio, Google APIs; snake_case — Slack, Twitter/X, Shopify (historically). If your team is mostly JavaScript/TypeScript, camelCase aligns naturally with the language. If Python, snake_case is more ergonomic.
Response Shapes
Single Resource
// GET /v1/users/usr_123
// Return the resource directly — no extra wrapper for single resources
{
"id": "usr_123",
"email": "ada@example.com",
"name": "Ada Lovelace",
"createdAt": "2024-01-15T09:00:00Z",
"plan": "pro"
}List Response with Envelope
// GET /v1/users?limit=20&cursor=eyJpZCI6MTAwfQ
{
"data": [
{ "id": "usr_123", "email": "ada@example.com", "name": "Ada Lovelace" },
{ "id": "usr_124", "email": "alan@example.com", "name": "Alan Turing" }
],
"pagination": {
"hasMore": true,
"nextCursor": "eyJpZCI6MTI0fQ",
"count": 2
}
}Pagination Patterns
Offset Pagination (simple, but fragile)
// GET /v1/orders?page=2&limit=20
{
"data": [...],
"pagination": {
"page": 2,
"limit": 20,
"total": 143,
"totalPages": 8
}
}
// Problem: if a new order is inserted on page 1 while the client is reading page 2,
// the last item from page 1 shifts to page 2 → duplicate in next requestCursor Pagination (stable, recommended)
// GET /v1/orders?limit=20
{
"data": [...],
"pagination": {
"hasMore": true,
"nextCursor": "eyJpZCI6MTAwLCJjcmVhdGVkQXQiOiIyMDI0LTAzLTE1In0="
}
}
// GET /v1/orders?limit=20&cursor=eyJpZCI6MTAwLCJjcmVhdGVkQXQiOiIyMDI0LTAzLTE1In0=
// Returns items strictly AFTER the cursor position — no duplicates regardless of inserts
// Server-side: decode cursor to get last seen ID/timestamp, then:
// WHERE (created_at, id) < (cursor.createdAt, cursor.id) ORDER BY created_at DESC LIMIT 21
// (fetch limit+1 to know if hasMore is true)The cursor is opaque to the client — base64-encode a JSON object containing the sort key(s) and primary key. This makes cursor pagination compatible with any sort order.
Error Response Design
Consistent error shapes make client error handling vastly simpler. Return errors in the same structure regardless of the HTTP status code.
// Validation error — HTTP 422
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body contains invalid fields",
"details": [
{ "field": "email", "message": "Must be a valid email address", "value": "not-an-email" },
{ "field": "age", "message": "Must be a positive integer", "value": -1 }
]
}
}
// Not found — HTTP 404
{
"error": {
"code": "NOT_FOUND",
"message": "User usr_999 not found"
}
}
// Unauthorized — HTTP 401
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid API key"
}
}
// Rate limited — HTTP 429 (include retry guidance in headers)
{
"error": {
"code": "RATE_LIMITED",
"message": "Too many requests",
"retryAfter": 30
}
}RFC 9457 Problem Details
// Content-Type: application/problem+json
// Standardized format for HTTP API errors (RFC 9457, formerly RFC 7807)
{
"type": "https://example.com/errors/validation-error",
"title": "Validation Error",
"status": 422,
"detail": "The request body contains invalid fields",
"instance": "/requests/123e4567-e89b-12d3-a456-426614174000",
"errors": [
{ "field": "email", "message": "Must be a valid email address" }
]
}Date and Time Formats
// Always use ISO 8601 UTC strings
{
"createdAt": "2024-03-15T14:30:00Z", // UTC (recommended)
"scheduledAt": "2024-03-15T14:30:00+05:30", // with timezone offset
"birthDate": "1990-07-12", // date only (no time component)
"duration": "PT1H30M" // ISO 8601 duration
}
// Never use:
{
"created_at": 1710509400, // Unix seconds — opaque to humans
"created_at_ms": 1710509400000, // Unix ms — confusing with seconds
"created_at": "March 15, 2024", // locale-specific — ambiguous
"created_at": "15/03/2024" // D/M/Y vs M/D/Y — ambiguous
}API Versioning
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /v1/users | Explicit, cacheable, easy to test | URL pollution |
| Query param | /users?v=2 | Clean URLs | Hard to cache, easy to forget |
| Header | Accept: application/vnd.api.v2+json | Purest REST semantics | Hard to test in browser, complex |
| No versioning | Additive changes only | Simple, no migration | Impossible to make breaking changes |
// URL path versioning — recommended for most teams
GET /v1/users/123
GET /v2/users/123 // new response shape
// Deprecate v1 using Sunset header
HTTP/1.1 200 OK
Sunset: Sat, 01 Jun 2025 00:00:00 GMT
Deprecation: Thu, 01 Jan 2025 00:00:00 GMT
Link: <https://api.example.com/v2/users>; rel="successor-version"Non-breaking (safe) changes: adding optional fields, adding new endpoints, adding new enum values (with caution), relaxing validation. Breaking changes require a version bump: removing fields, changing field types, renaming fields, changing HTTP method, changing status codes, making optional fields required.
OpenAPI Specification
# openapi.yaml (design-first approach)
openapi: 3.1.0
info:
title: My API
version: 1.0.0
paths:
/v1/users/{id}:
get:
summary: Get a user
parameters:
- name: id
in: path
required: true
schema: { type: string }
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'404':
$ref: '#/components/responses/NotFound'
components:
schemas:
User:
type: object
required: [id, email, createdAt]
properties:
id: { type: string, example: "usr_123" }
email: { type: string, format: email }
createdAt: { type: string, format: date-time }
responses:
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'FAQ
Should JSON API fields use camelCase or snake_case?
Both are used by major APIs — camelCase (Stripe, GitHub, Twilio) and snake_case (Slack, Twitter/X). The most important rule is consistency: never mix them in the same API. For JavaScript/TypeScript teams, camelCase is ergonomic since it matches the language convention. For Python teams, snake_case is more natural. Whatever you choose, enforce it with an API linter or JSON Schema pattern validation to catch violations early.
What is the best pagination pattern for a JSON API — cursor or offset?
Cursor-based pagination is more stable and is recommended for most production APIs. Offset pagination (page=2&limit=20) breaks when items are inserted or deleted between requests — the client sees duplicates or skips items. Cursor pagination uses an opaque position marker that remains valid regardless of inserts and deletions. The tradeoff: cursor pagination doesn't support jumping to arbitrary pages. Use offset for admin dashboards with mostly static data; use cursor for user-facing feeds, search results, or any data that updates frequently.
What should a JSON error response look like?
At minimum: a machine-readable error code (a string constant like "VALIDATION_ERROR"), a human-readable message for developers, and for validation errors, a details array with per-field error objects. Never expose stack traces or internal database errors in production. Consider adopting RFC 9457 (Problem Details) for a standardized cross-API error format using application/problem+json content type. The critical requirement: every error response must follow the same shape so clients can write a single error handler.
How should I version a JSON API?
URL path versioning (/v1/, /v2/) is the most pragmatic approach: it's visible in URLs, easy to test with curl, cacheable by proxies, and understood by every HTTP tool. Add the Sunset header to deprecated versions to signal to clients when they must migrate. Never make breaking changes (removing fields, changing types) without a version bump. Additive changes (new optional fields, new endpoints) are safe to make without versioning.
How should I represent dates in a JSON API?
Always use ISO 8601 UTC strings: "2024-03-15T14:30:00Z". Avoid Unix timestamps (opaque to humans), locale-specific formats (ambiguous), and formats without timezone indicators. ISO 8601 is human-readable, lexicographically sortable, supported by every language's standard library, and the format expected by JSON Schema's format: date-time. For date-only fields, use "2024-03-15". Document your timezone policy clearly in the API specification.
Should I wrap API responses in an envelope?
A common hybrid: return single resources directly (no envelope), but wrap list responses in {"{ data: [...], pagination: {...} }"} to include pagination metadata. Pure REST purists return resources directly and use HTTP headers for metadata (Link, X-Total-Count), but most modern APIs use a lightweight envelope for lists. The key principle: whatever you choose, apply it consistently — don't return bare arrays for some endpoints and wrapped objects for others.
How do I handle null vs omitted fields in JSON API responses?
null and omitted have different semantics: null means "this field has no value"; omitted means "this field is not included in this response". Be explicit: define in your API contract whether optional fields are always present as null or may be omitted. Consistently returning null (rather than omitting) is safer — clients don't need to check if ('field' in response) before accessing a field. Document the difference between fields that are always present, fields that may be null, and fields only returned in specific response contexts (e.g., detail vs summary).
What is OpenAPI and how does it relate to JSON API design?
OpenAPI is a standard specification format (YAML or JSON) for describing REST APIs. From an OpenAPI spec you can generate: interactive documentation (Swagger UI, Redoc), client SDKs in any language, mock servers, and validation middleware. OpenAPI 3.1 uses JSON Schema 2020-12, so your schema validator and your API spec share the same dialect. The design-first workflow writes the OpenAPI spec before implementing the API, allowing frontend and backend teams to work in parallel against the spec. This approach surfaces design inconsistencies early and produces better APIs than code-first approaches where the spec is generated from implementation.
Further reading and primary sources
- RFC 9457 — Problem Details for HTTP APIs — Standard error response format: application/problem+json with type, title, status, detail
- OpenAPI Specification 3.1 — The authoritative OpenAPI 3.1 specification with JSON Schema 2020-12 integration
- Google API Design Guide — Google's REST API design principles: naming, errors, pagination, versioning
- JSON:API Specification (Jsonic) — The JSON:API spec: resource objects, relationships, compound documents, and pagination
- REST API JSON Response (Jsonic) — Response shape patterns, status codes, and Content-Type conventions for REST APIs