DEV Community

Deepak Kumar
Deepak Kumar

Posted on Originally published at jsonformatterhub.com

REST API JSON Best Practices — Design Patterns Every Developer Should Know

A well-designed JSON API is a joy to consume. A poorly designed one creates bugs, confusion, and endless Slack threads asking "what does this field actually mean?" The decisions you make about naming, error formats, pagination, and date handling ripple through every client that ever calls your API. This covers the patterns that separate maintainable APIs from painful ones, with concrete JSON examples for each.

1. Field Naming Conventions

Pick snake_case or camelCase and be consistent across every endpoint. The worst choice is mixing them.

// snake_case (most common in Python/Ruby APIs)
{
  "user_id": 42,
  "first_name": "Alice",
  "last_login_at": "2025-05-01T10:30:00Z",
  "is_active": true
}
Enter fullscreen mode Exit fullscreen mode
// camelCase (most common in JavaScript/Node APIs)
{
  "userId": 42,
  "firstName": "Alice",
  "lastLoginAt": "2025-05-01T10:30:00Z",
  "isActive": true
}
Enter fullscreen mode Exit fullscreen mode

Both are fine. The critical rule: never mix. A response body with user_id alongside firstName is a maintenance nightmare.

Naming rules that hold regardless of case style:

  • Plural nouns for collections: users, orders, line_items
  • Boolean fields with is/has/can prefix: is_active, has_children, can_edit
  • No abbreviations: description not desc, quantity not qty
  • IDs as strings if they exceed 53 bits — JavaScript's Number can't safely represent integers above 2⁵³ − 1:
// IDs from distributed systems (Snowflake IDs, etc.) should be strings
{
  "id": "7823648237462834762",  // string  safe in all languages
  "order_number": 10045          // small integer  fine as number
}
Enter fullscreen mode Exit fullscreen mode

2. Response Structure

Wrap your response in a consistent envelope. Clients should be able to predict the shape of every response without reading the documentation.

// Single resource response
{
  "data": {
    "id": "usr_42",
    "name": "Alice",
    "email": "alice@example.com",
    "created_at": "2024-01-15T08:00:00Z"
  }
}
Enter fullscreen mode Exit fullscreen mode
// Collection response
{
  "data": [
    { "id": "usr_42", "name": "Alice" },
    { "id": "usr_43", "name": "Bob" }
  ],
  "meta": {
    "total": 248,
    "page": 1,
    "per_page": 20
  }
}
Enter fullscreen mode Exit fullscreen mode

The data wrapper lets you add meta, links, or other top-level fields later without breaking existing clients:

// BAD: bare array  can never add metadata without breaking clients
[
  { "id": 1, "name": "Alice" },
  { "id": 2, "name": "Bob" }
]

// GOOD: wrapped  extensible
{
  "data": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ]
}
Enter fullscreen mode Exit fullscreen mode

3. Error Response Format

Error responses are where the most inconsistency lives. Pick a format and enforce it via middleware so every error — including 500s — returns the same shape.

{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "Request body validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address", "value": "not-an-email" },
      { "field": "age", "message": "Must be a positive integer", "value": -5 }
    ],
    "request_id": "req_8f3k2j9s"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • code — machine-readable string constant clients can switch on. Never change these once published.
  • message — human-readable, can change. For debugging, not UI display.
  • details — array of field-level errors for validation failures.
  • request_id — correlates the error to a server log entry. Invaluable for support.
HTTP Status Error Code When to Use
400 VALIDATION_FAILED Request body or query params failed validation
401 UNAUTHORIZED No valid authentication credentials
403 FORBIDDEN Authenticated but not permitted
404 NOT_FOUND Resource does not exist
409 CONFLICT Optimistic lock failure, duplicate key
422 UNPROCESSABLE Syntactically valid but semantically wrong
429 RATE_LIMITED Too many requests
500 INTERNAL_ERROR Unexpected server error

Always pair the HTTP status code with a machine-readable code field. The status tells the client the category; the code tells it the specific cause.

4. Pagination

Three patterns exist, each with tradeoffs.

// Offset pagination  Request: GET /users?page=3&per_page=20
{
  "data": [ ],
  "meta": { "total": 248, "page": 3, "per_page": 20, "total_pages": 13 },
  "links": {
    "first": "/users?page=1&per_page=20",
    "prev": "/users?page=2&per_page=20",
    "next": "/users?page=4&per_page=20",
    "last": "/users?page=13&per_page=20"
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros: can jump to any page, easy to implement. Cons: skips or duplicates records if the dataset changes mid-pagination; slow on large tables (SQL OFFSET 10000 scans 10,000 rows).

// Cursor pagination  Request: GET /events?limit=20&after=cursor_abc123
{
  "data": [ ],
  "meta": { "has_next_page": true, "has_prev_page": true },
  "links": {
    "next": "/events?limit=20&after=cursor_xyz789",
    "prev": "/events?limit=20&before=cursor_abc123"
  }
}
Enter fullscreen mode Exit fullscreen mode

Pros: stable — new inserts don't cause duplicates or skips; efficient — keyset pagination uses an index. Cons: can't jump to page 7; total count is expensive to compute.

Use cursor pagination for feeds, timelines, and any high-write collection. Use offset pagination for admin tables where users need to jump to specific pages. Always include next/prev links in the response so clients don't have to construct URLs themselves.

5. Dates and Times

Use ISO 8601 with UTC timezone for all timestamps. Full stop.

// GOOD
{
  "created_at": "2025-05-01T10:30:00Z",       // UTC
  "scheduled_at": "2025-06-15T14:00:00+05:30", // with offset if timezone matters
  "birth_date": "1990-07-22"                   // date-only when time is irrelevant
}

// BAD
{
  "created_at": "May 1, 2025",        // ambiguous, not machine-parseable
  "created_at": 1746094200,           // Unix timestamp  readable but not self-documenting
  "created_at": "01/05/2025 10:30"    // is that May 1 or January 5?
}
Enter fullscreen mode Exit fullscreen mode
  • Always include time zone — 2025-05-01T10:30:00 without one is ambiguous.
  • Use Z suffix for UTC; use +HH:MM offset only when local timezone is meaningful.
  • Use date-only format (YYYY-MM-DD) when time of day genuinely doesn't matter.
  • Never use Unix timestamps as your primary format — they require documentation to interpret.

For durations, always encode the unit in the field name:

{
  "timeout_seconds": 30,
  "retry_interval_ms": 500,
  "session_duration": "PT1H30M"
}
Enter fullscreen mode Exit fullscreen mode

timeout: 30 is seconds? milliseconds? minutes? You'll get it wrong and break something.

6. Null vs Missing Fields

null and a missing field mean different things.

// Field present and null  the value exists, explicitly set to null
{ "name": "Alice", "middle_name": null }

// Field absent  you don't know or it doesn't apply
{ "name": "Alice" }
Enter fullscreen mode Exit fullscreen mode
  • Include with null when the field is part of the resource schema but has no value — clients can rely on the key always being present.
  • Omit entirely for optional metadata that may not apply.
  • Never use empty string as null — an empty string is a valid value (zero characters), not a sentinel for "no value."

7. Versioning

Plan for breaking changes from day one.

GET /v1/users
GET /v2/users
Enter fullscreen mode Exit fullscreen mode

URL versioning is simple, explicit, and cacheable — the version is visible in every log entry. Header versioning (Accept: application/vnd.myapi.v2+json) keeps URLs clean but requires clients to set headers correctly.

Breaking Non-breaking (safe to ship)
Removing a field Adding a new optional field
Renaming a field Adding a new endpoint
Changing a field's type Adding a new HTTP method to an endpoint
Changing error codes Making a required field optional
Changing auth requirements Adding new valid enum values (controversial)

8. Anti-Patterns to Avoid

Using HTTP 200 for errors:

// BAD  client has to parse body to detect failure
HTTP 200 OK
{ "success": false, "error": "User not found" }

// GOOD  HTTP status communicates outcome
HTTP 404 Not Found
{ "error": { "code": "NOT_FOUND", "message": "User with id usr_999 does not exist" } }
Enter fullscreen mode Exit fullscreen mode

Inconsistent ID types across endpoints:

// BAD
GET /users/42  { "id": 42 }
GET /orders/42  { "id": "ord_42" }

// GOOD  consistent format everywhere
GET /users/usr_42  { "id": "usr_42" }
GET /orders/ord_42  { "id": "ord_42" }
Enter fullscreen mode Exit fullscreen mode

Encoding data in field names:

// BAD  field names change with data
{ "price_usd": 10.99, "price_eur": 9.50, "price_gbp": 8.75 }

// GOOD  structure encodes the variation
{ "prices": [
  { "currency": "USD", "amount": 10.99 },
  { "currency": "EUR", "amount": 9.50 },
  { "currency": "GBP", "amount": 8.75 }
] }
Enter fullscreen mode Exit fullscreen mode

Deeply nested responses:

// BAD  three levels deep to reach the data
{ "response": { "body": { "result": { "users": [] } } } }

// GOOD  data at predictable depth
{ "data": [] }
Enter fullscreen mode Exit fullscreen mode

Exposing internal implementation details:

// BAD  reveals DB column names, ORM internals
{ "tbl_usr_id": 42, "__v": 0, "_id": "507f1f77bcf86cd799439011" }

// GOOD  API shape is independent of storage
{ "id": "usr_42", "created_at": "2025-05-01T10:30:00Z" }
Enter fullscreen mode Exit fullscreen mode

Summary

Concern Recommendation
Field naming snake_case or camelCase — pick one, enforce it everywhere
Response shape Wrap in data key; never bare arrays at top level
Error format Consistent envelope: code, message, details, request_id
HTTP status codes Use them correctly — 200 means success
Pagination Cursor for feeds; offset for admin; always include links
Dates ISO 8601 with UTC (Z suffix), always
Null handling null = known absence; omit = not applicable
IDs Strings for large IDs; consistent prefix per resource type
Versioning URL versioning for simplicity; plan for breaking changes

Well-designed JSON APIs aren't just easier to consume — they're easier to maintain. When the response shape is consistent, clients are more resilient, debugging is faster, and adding new features doesn't require coordinating SDK changes across every consumer.


If you're debugging an API response right now, JSON Formatter Hub formats, validates, and lets you explore it in a tree/table view — entirely in your browser.

Top comments (0)