REST API Design Principles
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.
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:
/usersnot/user - Use kebab-case:
/order-itemsnot/orderItems - Nest only one level deep — beyond that, use query params or top-level resources
- Never put verbs in URLs:
POST /usersnotPOST /createUser
Status Codes: Mean What You Say
Use Status Codes Properly
| 201 Created | Resource successfully created (POST) |
| 204 No Content | Success, no body (DELETE) |
| 400 Bad Request | Malformed request (invalid JSON, missing fields) |
| 401 Unauthorized | Not authenticated (who are you?) |
| 403 Forbidden | Authenticated but not authorized (you can't do this) |
| 404 Not Found | Resource doesn't exist |
| 409 Conflict | State conflict (duplicate email, version mismatch) |
| 422 Unprocessable | Valid JSON but fails validation (email format wrong) |
| 429 Too Many Requests | Rate limited |
| 500 Internal Error | Server 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, andparam(which field caused it) - Idempotency keys: POST requests accept an
Idempotency-Keyheader — 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:
Linkheader withrel="next",rel="last"— standard HTTP, works with any client - Rate limiting:
X-RateLimit-Remaining,X-RateLimit-Resetheaders on every response - Conditional requests:
ETagandIf-None-Match— 304 Not Modified saves bandwidth - Hypermedia: Every response includes
urlfields 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.