REST API Design Principles

📘 Chapter 11: API Design ⏱️ 9 min read 🏗️ Lesson 045

REST — Representational State Transfer — is an architectural style, not a protocol. Defined by Roy Fielding in his 2000 dissertation, it describes how the web already works and provides constraints that make APIs predictable, scalable, and cacheable. Most "REST APIs" you encounter only partially follow these principles — understanding the full picture helps you design APIs that developers actually enjoy using.

Core Principles

The Six REST Constraints

  • Client-Server: Separate concerns — clients handle UI, servers handle data storage. They evolve independently.
  • Stateless: Each request contains all information needed to process it. No server-side session state between requests.
  • Cacheable: Responses must declare themselves cacheable or non-cacheable. Clients and intermediaries can cache appropriately.
  • Uniform Interface: A consistent way to interact with resources (the most important constraint — covered below).
  • Layered System: Clients can't tell if they're connected directly to the server or through intermediaries (proxies, load balancers, CDNs).
  • Code on Demand (optional): Servers can extend client functionality by sending executable code (e.g., JavaScript).

Resource-Oriented Design

The fundamental concept: URLs represent resources (nouns), and HTTP methods represent actions (verbs). You don't design endpoints around actions — you design them around the things your API manages.

CRUD → HTTP Methods Mapping Operation Method URL Pattern Response Create POST /users 201 Created + Location Read (list) GET /users 200 OK + array Read (single) GET /users/{id} 200 OK + object Update (full) PUT /users/{id} 200 OK + updated obj Update (partial) PATCH /users/{id} 200 OK + updated obj Delete DELETE /users/{id} 204 No Content
Figure 1: HTTP methods map cleanly to CRUD operations. The URL identifies the resource; the method says what to do with it.

URL Design

Hierarchical, Predictable, Consistent

Good REST URLs tell a story about resource relationships:

GET  /users                      # All users
GET  /users/42                   # User 42
GET  /users/42/orders            # All orders for user 42
GET  /users/42/orders/7          # Order 7 for user 42
POST /users/42/orders            # Create new order for user 42

Rules of thumb:

  • Use plural nouns: /users not /user
  • Use kebab-case: /order-items not /orderItems
  • Nest only one level deep — beyond that, use query params or top-level resources
  • Never put verbs in URLs: POST /users not POST /createUser

Status Codes: Mean What You Say

Use Status Codes Properly

201 CreatedResource successfully created (POST)
204 No ContentSuccess, no body (DELETE)
400 Bad RequestMalformed request (invalid JSON, missing fields)
401 UnauthorizedNot authenticated (who are you?)
403 ForbiddenAuthenticated but not authorized (you can't do this)
404 Not FoundResource doesn't exist
409 ConflictState conflict (duplicate email, version mismatch)
422 UnprocessableValid JSON but fails validation (email format wrong)
429 Too Many RequestsRate limited
500 Internal ErrorServer bug (never intentional)

Pagination

Cursor-Based vs Offset-Based

Offset-based (?page=3&limit=20): Simple but breaks when data changes between pages — items can be skipped or duplicated.

Cursor-based (?after=eyJpZCI6NDJ9&limit=20): Uses an opaque cursor pointing to the last item seen. Stable pagination even when data changes.

// Cursor-based response
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6NjJ9",
    "has_more": true
  }
}

Why cursor wins for large datasets: Offset requires OFFSET 10000 which forces the DB to scan and discard 10,000 rows. Cursor uses an indexed WHERE id > 42 — constant time regardless of page number.

Filtering, Sorting & Field Selection

# Filtering
GET /orders?status=shipped&created_after=2024-01-01

# Sorting
GET /users?sort=-created_at,name    # descending created_at, then ascending name

# Field selection (sparse fieldsets)
GET /users/42?fields=id,name,email  # Only return these fields

HATEOAS: Hypermedia Links

Self-Documenting APIs

HATEOAS (Hypermedia As The Engine Of Application State) means responses include links to related actions and resources — clients don't need to hardcode URLs.

{
  "id": 42,
  "name": "Alice",
  "email": "alice@example.com",
  "_links": {
    "self": "/users/42",
    "orders": "/users/42/orders",
    "update": { "href": "/users/42", "method": "PATCH" },
    "delete": { "href": "/users/42", "method": "DELETE" }
  }
}

This makes APIs discoverable — a client can navigate from any entry point by following links, much like browsing the web.

Error Response Format

Structured, Actionable Errors

{
  "error": {
    "code": "validation_error",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Must be a valid email address" },
      { "field": "age", "message": "Must be at least 18" }
    ],
    "request_id": "req_abc123",
    "docs": "https://api.example.com/docs/errors#validation_error"
  }
}

Good errors include: machine-readable code, human-readable message, field-level details, request ID for debugging, and a link to documentation.

🌍 Stripe's Gold-Standard API

Stripe's API is widely considered the best-designed REST API in production:

  • Consistent naming: Every resource uses the same patterns — /v1/customers, /v1/charges, /v1/subscriptions
  • Clear errors: Every error has a type, code, message, and param (which field caused it)
  • Idempotency keys: POST requests accept an Idempotency-Key header — safe to retry without double-charging
  • Versioning: Date-based versions (e.g., 2024-01-15) — each account pins to its creation version
  • Expandable objects: GET /charges/ch_123?expand[]=customer — inline related objects without separate requests

🌍 GitHub's API

GitHub's REST API demonstrates excellent use of HTTP semantics:

  • Pagination: Link header with rel="next", rel="last" — standard HTTP, works with any client
  • Rate limiting: X-RateLimit-Remaining, X-RateLimit-Reset headers on every response
  • Conditional requests: ETag and If-None-Match — 304 Not Modified saves bandwidth
  • Hypermedia: Every response includes url fields pointing to related resources

Interactive: Design a REST API

🎮 API Design Challenge

Design a REST API for the given domain. Choose the right URL structure, HTTP method, and status code.