Working with JSON in REST APIs

Well-designed JSON APIs are the backbone of modern web and mobile applications. Whether you are building a public API consumed by third-party developers or an internal service for your microservices architecture, following established patterns makes your API predictable, maintainable, and secure. This guide covers the most important patterns and best practices for designing JSON APIs.

Content-Type Headers

Every JSON API should properly set the Content-Type header on responses and require it on requests. The correct media type for JSON is application/json. APIs should also set the Accept header to indicate which response formats the client can handle.

// Request headers
Content-Type: application/json
Accept: application/json

// Response headers
Content-Type: application/json; charset=utf-8

If your API also implements the JSON:API specification, use application/vnd.api+json instead.

Request and Response Structure

Consistent envelope structures make APIs easier to consume. A common pattern wraps responses in a standard object with data, meta, and errors fields:

// Successful response
{
  "data": {
    "id": "usr_123",
    "name": "Alice",
    "email": "alice@example.com"
  },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2025-01-15T10:30:00Z"
  }
}

// Collection response
{
  "data": [
    { "id": "usr_123", "name": "Alice" },
    { "id": "usr_456", "name": "Bob" }
  ],
  "meta": {
    "total": 42,
    "page": 1,
    "perPage": 20
  }
}

Some APIs use a flat response for single resources and reserve the envelope for collections and errors. Either approach works as long as it is applied consistently.

Error Response Patterns

Error responses should be structured and machine-readable. The IETF's RFC 7807 Problem Details (updated by RFC 9457) provides a standard format for HTTP API error responses:

{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation Error",
  "status": 422,
  "detail": "The request body contains invalid fields.",
  "instance": "/users/usr_123",
  "errors": [
    {
      "field": "email",
      "message": "Must be a valid email address"
    },
    {
      "field": "age",
      "message": "Must be a positive integer"
    }
  ]
}

The type field is a URI that identifies the error category (it does not need to be a resolvable URL). The detail field provides a human-readable explanation. The status mirrors the HTTP status code. You can extend the object with additional fields like errors for field-level validation details.

Pagination

Any endpoint that returns a list of resources needs pagination. The three most common approaches are:

Offset-Based Pagination

The simplest approach uses offset and limit (or page and perPage) parameters. This is easy to implement but suffers from inconsistent results when data changes between requests — items can be skipped or duplicated.

GET /api/users?page=2&perPage=20

{
  "data": [...],
  "meta": {
    "total": 156,
    "page": 2,
    "perPage": 20,
    "totalPages": 8
  }
}

Cursor-Based Pagination

Cursor pagination uses an opaque token that represents a position in the dataset. It provides stable pagination even when data is being inserted or deleted, and performs better on large datasets because databases can use indexed seeks rather than offset scans.

GET /api/users?cursor=eyJpZCI6NDJ9&limit=20

{
  "data": [...],
  "meta": {
    "hasMore": true,
    "nextCursor": "eyJpZCI6NjJ9"
  }
}

Link Header Pagination

Following the GitHub API model, pagination links can be provided in the HTTP Link header. This separates pagination metadata from the response body, keeping the data array clean:

Link: <https://api.example.com/users?page=3>; rel="next",
      <https://api.example.com/users?page=8>; rel="last"

Filtering and Sorting

APIs should provide clear conventions for filtering and sorting collections. Common patterns include:

# Simple equality filter
GET /api/users?role=admin&status=active

# Range filters
GET /api/products?price_min=10&price_max=50

# Sorting (prefix with - for descending)
GET /api/users?sort=name        # ascending by name
GET /api/users?sort=-createdAt  # descending by creation date

# Multiple sort fields
GET /api/users?sort=-role,name

Be explicit about which fields support filtering and sorting in your API documentation. Never allow arbitrary field access without validation, as this can expose sensitive data or enable denial-of-service through expensive queries.

Versioning Strategies

API versioning allows you to evolve your API without breaking existing clients. The main strategies are:

  • URL path versioning /api/v1/users. The most common and most visible approach. Easy to route and document, but violates REST purity since the same resource has different URIs.
  • Header versioning Accept: application/vnd.example.v2+json. Keeps URLs clean but is harder to test in browsers and less discoverable.
  • Query parameter versioning /api/users?version=2. Simple but can be accidentally cached without the version parameter.

URL path versioning is the most pragmatic choice for most teams. Regardless of strategy, maintain backward compatibility within a version and provide clear deprecation timelines when retiring old versions.

HATEOAS

Hypermedia as the Engine of Application State (HATEOAS) is a REST constraint where the server includes links in responses that tell the client what actions are available. This makes APIs self-navigable:

{
  "data": {
    "id": "order_789",
    "status": "pending",
    "total": 59.99
  },
  "links": {
    "self": "/api/orders/order_789",
    "cancel": "/api/orders/order_789/cancel",
    "payment": "/api/orders/order_789/pay",
    "items": "/api/orders/order_789/items"
  }
}

While full HATEOAS adoption is rare in practice, including key navigation links in responses reduces client-side URL construction and makes API evolution easier since link targets can change without breaking clients.

Partial Responses and Field Selection

For endpoints that return large resource representations, allow clients to request only the fields they need. This reduces payload size and improves performance, especially on mobile networks:

# Request only specific fields
GET /api/users/123?fields=id,name,email

# Google-style field masks
GET /api/users/123?fields=name,address(city,state)

# GraphQL-style (for comparison)
query {
  user(id: "123") {
    id
    name
    email
  }
}

Implement field selection carefully. Ensure that omitted fields are truly absent from the response (not present with null values), and always include the resource id regardless of the field selection.

Batch Operations

When clients need to create, update, or delete multiple resources at once, batch endpoints reduce the number of round trips:

// Batch create
POST /api/users/batch
{
  "operations": [
    { "method": "create", "body": { "name": "Alice", "email": "alice@example.com" } },
    { "method": "create", "body": { "name": "Bob", "email": "bob@example.com" } }
  ]
}

// Response with per-item status
{
  "results": [
    { "status": 201, "data": { "id": "usr_123", "name": "Alice" } },
    { "status": 409, "error": { "detail": "Email already exists" } }
  ]
}

Each operation in a batch should be processed independently and return its own status. Avoid making the entire batch transactional unless your business logic requires it, as partial success reporting gives clients better visibility into what happened.

JSON:API Specification Overview

The JSON:API specification defines a complete standard for building APIs in JSON. It prescribes specific structures for resources, relationships, links, pagination, filtering, sorting, and error handling. A JSON:API response looks like this:

{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "JSON API Design Patterns"
    },
    "relationships": {
      "author": {
        "data": { "type": "people", "id": "42" }
      }
    }
  },
  "included": [
    {
      "type": "people",
      "id": "42",
      "attributes": {
        "name": "Alice"
      }
    }
  ]
}

JSON:API is opinionated and adds structure overhead, but it eliminates bikeshedding about API design decisions and provides a rich ecosystem of client libraries that handle pagination, relationship loading, and caching automatically.

Security Best Practices

JSON APIs must be designed with security in mind from the start. Key practices include:

  • Input validation — Validate all incoming JSON against a schema. Reject requests with unexpected fields, wrong types, or values outside allowed ranges. Never trust client input.
  • Output encoding — Ensure JSON responses are properly encoded to prevent injection attacks. Set Content-Type: application/json to prevent browsers from interpreting responses as HTML.
  • Rate limiting — Protect endpoints from abuse with rate limiting. Return 429 Too Many Requests with Retry-After headers.
  • Depth and size limits — Set maximum limits on JSON nesting depth and payload size to prevent resource exhaustion attacks. A 1MB limit is reasonable for most APIs.
  • CORS configuration — Configure Cross-Origin Resource Sharing headers carefully. Only allow origins that need access, and be specific about allowed methods and headers.
  • Sensitive data — Never include passwords, tokens, or internal system details in JSON responses. Use field-level access control to restrict sensitive fields based on the caller's permissions.
  • JSON hijacking prevention — While modern browsers have mitigated classic JSON hijacking, always use X-Content-Type-Options: nosniff to prevent MIME type sniffing.

Related Guides