REST API design debates can get theological. Resource purity, HATEOAS, Richardson maturity levels — these discussions often miss the point. The goal is an API that’s easy to understand, predictable, and maintainable.

URL Structure

Use nouns for resources, HTTP methods for actions:

GET    /api/users          # List users
GET    /api/users/42       # Get one user
POST   /api/users          # Create a user
PUT    /api/users/42       # Replace a user
PATCH  /api/users/42       # Update fields on a user
DELETE /api/users/42       # Delete a user

For actions that don’t map cleanly to CRUD, use a sub-resource:

POST /api/users/42/deactivate
POST /api/orders/99/refund

This is more pragmatic than inventing a resource that represents the concept of deactivation.

Pagination

Always paginate list endpoints. Cursor-based pagination is more robust than offset-based:

{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTAwfQ==",
    "has_more": true
  }
}

Cursors don’t break when items are inserted or deleted during iteration. Offsets do.

Error Responses

Return consistent error structures:

{
  "error": {
    "code": "validation_error",
    "message": "Validation failed",
    "details": [
      { "field": "email", "message": "must be a valid email address" },
      { "field": "name", "message": "is required" }
    ]
  }
}

Use appropriate status codes:

  • 400 — Client sent invalid data
  • 401 — Not authenticated
  • 403 — Authenticated but not authorized
  • 404 — Resource doesn’t exist
  • 409 — Conflict (duplicate, stale update)
  • 422 — Valid syntax but unprocessable content
  • 429 — Rate limited
  • 500 — Server error (always your bug)

Versioning

Put the major version in the URL: /api/v1/users. It’s visible, easy to route, and works with every HTTP client.

Header-based versioning (Accept: application/vnd.myapi.v2+json) is more “correct” but causes real-world problems with caching, debugging, and tooling.

Filtering and Sorting

Use query parameters:

GET /api/users?status=active&sort=-created_at&limit=20

Prefix with - for descending sort. Support multiple sort fields separated by commas. Document the available filter fields explicitly.

Response Shaping

Let clients request only the fields they need:

GET /api/users/42?fields=id,name,email

This reduces payload size and avoids over-fetching. It’s simpler than maintaining multiple endpoints for different use cases.

Design for the developers who will consume your API. Clear documentation, consistent patterns, and helpful error messages matter more than architectural purity.