alltools.one
APIβ€’
2025-06-30
β€’
9 min
β€’
alltools.one Team
JSONAPIRESTDesign PatternsBackend

JSON API Design Patterns: Building Better REST APIs

A well-designed API is a joy to use. A poorly designed one creates bugs, frustration, and technical debt. This guide covers battle-tested patterns for designing JSON REST APIs that are consistent, discoverable, and maintainable.

Resource Naming

Resources are nouns, not verbs. Use plural nouns for collections and singular resources via ID:

GET    /api/users          # List users
POST   /api/users          # Create a user
GET    /api/users/123      # Get user 123
PUT    /api/users/123      # Update user 123
DELETE /api/users/123      # Delete user 123

Naming conventions:

  • Use lowercase with hyphens: /api/blog-posts (not blogPosts or blog_posts)
  • Nest related resources: /api/users/123/orders
  • Limit nesting to 2 levels: /api/users/123/orders/456 (not deeper)
  • Avoid verbs in URLs: /api/users/123/activate is acceptable for actions that don't map to CRUD

Response Envelope

Wrap responses in a consistent envelope:

{
  "data": {
    "id": "123",
    "type": "user",
    "attributes": {
      "name": "Alice",
      "email": "alice@example.com",
      "createdAt": "2024-01-15T10:30:00Z"
    }
  },
  "meta": {
    "requestId": "req_abc123"
  }
}

For collections:

{
  "data": [
    { "id": "123", "name": "Alice" },
    { "id": "456", "name": "Bob" }
  ],
  "meta": {
    "total": 142,
    "page": 1,
    "perPage": 20
  }
}

Validate your API responses with our JSON Validator to ensure they match your schema.

Pagination

Three common approaches:

Offset-Based (Simplest)

GET /api/users?page=2&per_page=20
{
  "data": [...],
  "meta": {
    "page": 2,
    "perPage": 20,
    "total": 142,
    "totalPages": 8
  }
}

Pro: Simple, supports jumping to any page. Con: Inconsistent results with concurrent inserts/deletes.

Cursor-Based (Most Reliable)

GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
{
  "data": [...],
  "meta": {
    "hasNext": true,
    "nextCursor": "eyJpZCI6MTQzfQ"
  }
}

Pro: Consistent with concurrent changes, performant on large datasets. Con: Cannot jump to arbitrary pages.

Keyset-Based (Most Performant)

GET /api/users?after_id=123&limit=20

Uses the last item's ID (or other sorted field) to fetch the next page. Similar to cursor-based but with transparent parameters.

Recommendation: Use cursor-based for real-time feeds and large datasets. Use offset-based for admin dashboards where page jumping matters.

Filtering and Sorting

Filtering

GET /api/users?status=active&role=admin
GET /api/users?created_after=2024-01-01
GET /api/users?search=alice

For complex filters, consider a dedicated query parameter:

GET /api/users?filter[status]=active&filter[role]=admin

Sorting

GET /api/users?sort=name          # Ascending
GET /api/users?sort=-created_at   # Descending (prefix with -)
GET /api/users?sort=-created_at,name  # Multiple fields

Error Handling

Consistent error responses are critical for API usability:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address",
        "value": "not-an-email"
      },
      {
        "field": "age",
        "message": "Must be between 0 and 150",
        "value": -5
      }
    ]
  },
  "meta": {
    "requestId": "req_abc123"
  }
}

HTTP status codes to use:

CodeMeaningWhen
200OKSuccessful GET, PUT
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestValidation errors
401UnauthorizedMissing/invalid auth
403ForbiddenValid auth, insufficient permissions
404Not FoundResource does not exist
409ConflictDuplicate resource, version conflict
422UnprocessableSemantically invalid
429Too Many RequestsRate limit exceeded
500Internal ErrorUnexpected server error

Versioning

Three approaches:

URL Path (Recommended)

GET /api/v1/users
GET /api/v2/users

Pro: Explicit, easy to route, easy to test.

Header-Based

GET /api/users
Accept: application/vnd.myapi.v2+json

Pro: Clean URLs. Con: Harder to test, less discoverable.

Query Parameter

GET /api/users?version=2

Pro: Easy to test. Con: Optional parameter semantics.

Recommendation: URL path versioning is the most practical choice. It's explicit, cacheable, and works with every HTTP tool.

Date and Time Handling

Always use ISO 8601 in UTC:

{
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-01-15T14:22:33Z",
  "expiresAt": "2024-12-31T23:59:59Z"
}

Never use Unix timestamps in API responses β€” they are ambiguous (seconds vs milliseconds) and not human-readable. For more on timestamp handling, see our Unix Timestamps guide.

Rate Limiting

Communicate rate limits in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1705312800
Retry-After: 30

Return 429 Too Many Requests when the limit is exceeded, with a clear error message and Retry-After header.

HATEOAS (Optional but Powerful)

Include links to related resources and actions:

{
  "data": {
    "id": "123",
    "name": "Alice",
    "links": {
      "self": "/api/users/123",
      "orders": "/api/users/123/orders",
      "avatar": "/api/users/123/avatar"
    }
  }
}

This makes your API self-documenting and reduces client-side URL construction.

FAQ

Should I use JSON:API specification or design my own format?

The JSON:API specification (jsonapi.org) provides a comprehensive standard, but it can be verbose for simple APIs. For most projects, designing a simpler custom format following the patterns in this guide is more practical. Use JSON:API if you need automatic client libraries and a strict standard.

How should I handle partial updates (PATCH vs PUT)?

Use PUT for complete resource replacement (client sends all fields). Use PATCH for partial updates (client sends only changed fields). PATCH with JSON Merge Patch (RFC 7396) is the simplest approach: send a JSON object with only the fields to update, and null to remove a field.

Related Resources

Published on 2025-06-30
JSON API Design Patterns: Building Better REST APIs | alltools.one