API Design

You are designing or reviewing: $ARGUMENTS

API design is a contract with every consumer — external developers, mobile apps, browser clients, and future yourself. Changing an API is expensive. Designing it right the first time saves months of migration work.


Principles

  1. Design for the consumer, not the implementation. Resources should match how clients think about the problem, not how the database is structured.
  2. Be consistent. The tenth endpoint you add must follow the same patterns as the first.
  3. Be boring. Clever API design is bad API design. Every deviation from convention requires documentation.
  4. Fail loudly and clearly. Ambiguous error responses are support tickets.
  5. Treat URLs as public commitments. Never break a URL that is already in use.

1. URL Design

Resource naming

  • Use nouns, not verbs: /users not /getUsers
  • Use plural nouns: /orders, /products, /invoices
  • Use kebab-case for multi-word: /api/line-items, /api/password-resets
  • Nest resources sparingly — max 2 levels deep
GET  /api/users              → list users
POST /api/users              → create user
GET  /api/users/:id          → get one user
PUT  /api/users/:id          → replace user (full update)
PATCH /api/users/:id         → partial update
DELETE /api/users/:id        → delete user

# Nested (1 level ok, 2 levels acceptable, 3+ is a smell)
GET  /api/users/:id/orders   → orders belonging to user

Actions that don't map to CRUD

Use a _actions sub-resource with a POST:

POST /api/users/:id/actions/verify-email
POST /api/orders/:id/actions/cancel
POST /api/invoices/:id/actions/send

Never: POST /api/cancelOrder, GET /api/activateUser?id=123


2. HTTP Method Semantics

Method Meaning Idempotent Safe
GET Retrieve resource
POST Create resource / trigger action
PUT Replace resource entirely
PATCH Partial update Depends
DELETE Remove resource

Rules:

  • GET never changes state (no GET /api/users/activate?token=X)
  • POST for creation returns 201 Created with the created resource in body
  • PUT requires the full resource in the body (any omitted field is set to null/default)
  • PATCH accepts only the fields being changed
  • DELETE on an already-deleted resource returns 404 or 204 (pick one, be consistent)

3. HTTP Status Codes

Use the right code. Not just 200 and 500.

Code Use for
200 OK Successful GET, PATCH, PUT, DELETE (when returning body)
201 Created Successful POST that created a resource
202 Accepted Request queued for async processing
204 No Content Successful DELETE or action with no response body
400 Bad Request Malformed JSON, invalid field type
401 Unauthorized Not authenticated (no valid token)
403 Forbidden Authenticated but not authorised for this resource
404 Not Found Resource does not exist
409 Conflict Uniqueness conflict, optimistic lock conflict
422 Unprocessable Entity Validation failed (correct JSON, invalid values)
429 Too Many Requests Rate limit exceeded
500 Internal Server Error Unexpected server error (never send detail to clients)
503 Service Unavailable Temporary maintenance or overload

4. Request and Response Shapes

Response envelope (consistent format)

// Success — single resource
HTTP 200
{
  "data": {
    "id": "uuid",
    "email": "user@example.com",
    "created_at": "2026-01-01T12:00:00Z"
  }
}

// Success — collection
HTTP 200
{
  "data": [...],
  "meta": {
    "total": 1234,
    "page": 1,
    "per_page": 20,
    "last_page": 62
  }
}

// Error
HTTP 422
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email has already been taken."],
    "password": ["The password must be at least 8 characters."]
  }
}

Field naming

  • Use snake_case for JSON fields (most universal)
  • Use ISO 8601 for all dates: "2026-03-28T14:30:00Z" (UTC, explicit timezone)
  • Use strings for IDs if they may become non-numeric: "id": "abc123" not "id": 1
  • Never use data as a field name inside data (nested envelope anti-pattern)

5. Pagination

Use cursor-based pagination for large or frequently-updating datasets, offset-based for simple admin pages:

Offset pagination

GET /api/orders?page=3&per_page=20

Response:
{
  "data": [...],
  "meta": { "total": 500, "page": 3, "per_page": 20, "last_page": 25 }
}

Cursor pagination (better for infinite scroll / large sets)

GET /api/feed?cursor=eyJpZCI6MTIzfQ&limit=20

Response:
{
  "data": [...],
  "meta": {
    "next_cursor": "eyJpZCI6MTQzfQ",
    "has_more": true
  }
}

6. Filtering, Sorting, Search

GET /api/orders?status=shipped&created_after=2026-01-01
GET /api/products?sort=price&direction=asc
GET /api/users?q=john&status=active
GET /api/orders?include=customer,items   (sparse fieldsets / eager loading)

Document every supported filter, sort field, and include option. Fail with 400 for unsupported parameters.


7. Versioning

Never break existing clients. Use one of:

URL versioning (simplest, most explicit)

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

Header versioning (cleaner URLs)

Accept: application/vnd.api+json;version=2

Rules:

  • A new version is only needed when a breaking change is unavoidable (removing a field, changing a type, changing semantics)
  • Adding new optional fields or new endpoints is NOT a breaking change
  • Maintain old versions for at least 6 months after announcing deprecation
  • Communicate deprecation via a response header on the old endpoints: Deprecation: true, Sunset: Sat, 01 Jan 2027 00:00:00 GMT

8. Authentication

  • Use Authorization: Bearer <token> header — not query string (gets logged)
  • Use short-lived access tokens + long-lived refresh tokens
  • Return 401 when the token is missing or expired, 403 when it is valid but unauthorized
  • Never include tokens in URLs or redirect targets

9. API Design Review Checklist

  • [ ] Resources are nouns, URLs are consistent (/users/:id, not /user/:id and /users/:userId)
  • [ ] Correct HTTP method for each operation
  • [ ] Correct status codes for every response case
  • [ ] Consistent envelope format for success and error responses
  • [ ] All timestamps in ISO 8601 UTC
  • [ ] Pagination on every collection endpoint
  • [ ] Validation errors return 422 with field-level detail
  • [ ] No secrets or sensitive data in URLs (use request body or headers)
  • [ ] All endpoints document authentication requirements
  • [ ] Breaking changes are versioned