- REST (Representational State Transfer) is an architectural style for building networked APIs, described by Roy Fielding in his 2000 PhD dissertation. It is built on top of HTTP (topic 2) and inherits its semantics.
- REST is not a specification or standard — it's a set of constraints. An API that follows these constraints is called RESTful.
- The goal is uniform interface: any client that understands HTTP can use any REST API without a custom SDK, because the vocabulary (methods, status codes, URIs) is standardised.
- REST is the dominant style for public APIs, inter-service communication, and mobile backends.
REST's six constraints:
- Client-Server — separate concerns (topic 1).
- Stateless — every request contains all context needed; no server-side session.
- Cacheable — responses declare whether they can be cached.
- Uniform Interface — resources identified by URIs, representations transferred via standard media types, self-descriptive messages.
- Layered System — client can't tell if it's talking to the origin or an intermediary (CDN, load balancer).
- Code on Demand (optional) — server can send executable code (JavaScript to browsers).
sequenceDiagram
participant Mobile as Mobile App
participant CDN as CDN (Layer 5)
participant LB as Load Balancer (Layer 5)
participant API as API Server
participant DB as Database
Mobile->>CDN: GET /api/v1/products/42
CDN-->>Mobile: 200 OK (from cache, Cache-Control: s-maxage=60)
Mobile->>LB: POST /api/v1/orders (not cacheable)
LB->>API: POST /api/v1/orders
API->>DB: INSERT INTO orders ...
DB-->>API: order created
API-->>Mobile: 201 Created\nLocation: /api/v1/orders/99
REST models resources (nouns), not operations (verbs). Resources are things you act on, not things you do.
| Bad (RPC style) | Good (REST style) | Reason |
|---|---|---|
| POST /createUser | POST /users | Verb in URL is redundant — POST already means create |
| GET /getUserById?id=42 | GET /users/42 | Path parameters for resource identity |
| POST /deleteOrder | DELETE /orders/42 | Use HTTP method for the operation |
| GET /getAllProducts | GET /products | "all" is implied — just return the collection |
| POST /updateUserPassword | PATCH /users/42/password | Resource path encodes what's being changed |
Rules:
- Use nouns, not verbs.
- Use lowercase with hyphens for readability:
/product-categoriesnot/productCategories. - Use plural nouns for collections:
/users,/orders,/products. - Use singular nouns for singletons:
/user/profile(profile of the current user),/configuration. - Nest for ownership up to 2 levels:
/users/42/orders(orders of user 42).
| Operation | HTTP Method | URI | Request Body | Success Response |
|---|---|---|---|---|
| List all orders | GET | /orders |
— | 200 + array |
| List with filter | GET | /orders?status=shipped&page=2 |
— | 200 + array |
| Get one order | GET | /orders/42 |
— | 200 + object |
| Create order | POST | /orders |
{items, address} |
201 + object + Location: /orders/42 |
| Replace order | PUT | /orders/42 |
full order object | 200 + object |
| Update order fields | PATCH | /orders/42 |
{status: "cancelled"} |
200 + object |
| Delete order | DELETE | /orders/42 |
— | 204 |
| Nested resource | GET | /users/42/orders |
— | 200 + array |
| Action on resource | POST | /orders/42/cancel |
optional {reason} |
200 + object |
The last row (/orders/42/cancel) is a pragmatic exception: sometimes an action (cancel, publish, archive) doesn't map cleanly to a method. Using POST /orders/42/cancel is acceptable and widely used.
Idempotency matters for safe retries in the face of network failures:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: DELETE /orders/42
Note over S: Network timeout — client doesn't know if it succeeded
C->>S: DELETE /orders/42 (retry — safe, idempotent)
S-->>C: 404 Not Found (order already deleted — that's fine)
Note over C: Client knows the order is gone — mission accomplished
vs. POST:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: POST /payments {amount: 100}
Note over S: Network timeout — client doesn't know if payment was created
C->>S: POST /payments {amount: 100} (retry — DANGEROUS)
S-->>C: 201 Created (second charge created — customer charged twice!)
Idempotency Key pattern for non-idempotent operations:
POST /payments
Idempotency-Key: a7f5c3e8-d4b2-4f9a-8c1e-3b7d2a5f9c8e
{"amount": 100, "currency": "USD"}
Server stores the result keyed by the idempotency key. If the same key is received again, return the cached result without re-processing. The client can safely retry after a timeout.
Returning all records in one response is impractical at scale. Three strategies:
GET /orders?offset=40&limit=20
Returns records 41–60.
{
"data": [...],
"pagination": {
"offset": 40,
"limit": 20,
"total": 243,
"next": "/orders?offset=60&limit=20",
"prev": "/orders?offset=20&limit=20"
}
}Pros: random access — jump to any page.
Cons: OFFSET 1000000 LIMIT 20 in SQL requires the database to count and skip 1 million rows — slow. Also inconsistent: if a record is inserted between page 1 and page 2 requests, some records shift and might be returned twice or skipped.
GET /orders?after=cursor_dXNlcjoxMDA&limit=20
The cursor encodes the last record seen (often a Base64-encoded ID or composite key, not a page number):
{
"data": [...],
"pagination": {
"next_cursor": "cursor_dXNlcjoxMjA",
"has_more": true
}
}Pros: O(log N) performance (uses index seek, not scan). Stable — insertions don't shift pages. Cons: no random access — can't jump to page 5. Previous page navigation is harder.
Similar to cursor, but exposes the sort key directly:
GET /orders?after_id=1000&limit=20
Most practical for ORDER BY id queries. Uses the primary key index directly.
- Default: cursor pagination — performant, stable, sufficient for most UIs.
- Need random page access: offset pagination — accept the performance cost.
- Internal APIs with trusted clients: keyset pagination — simplest to implement correctly.
Filtering (query parameters):
GET /orders?status=shipped&user_id=42&min_total=50
GET /orders?created_after=2024-01-01T00:00:00Z
Sorting:
GET /orders?sort=created_at&order=desc
GET /orders?sort=total&order=asc
Multi-sort (comma-separated):
GET /orders?sort=status,created_at&order=asc,desc
Field selection (sparse fieldsets — avoids over-fetching):
GET /orders?fields=id,status,total
Response only includes the requested fields. Reduces payload, speeds up serialisation.
Embedding related resources (avoids N+1 round trips):
GET /orders/42?include=items,user
Server includes items and user in the response instead of requiring separate requests.
HTTP allows clients to tell servers what format they can accept:
GET /orders/42
Accept: application/json, application/xml;q=0.9, */*;q=0.8
Accept-Language: en-US, en;q=0.9, de;q=0.8
Accept-Encoding: gzip, br
q values (quality factors) express preference: q=1.0 (default) = preferred, q=0.5 = acceptable, q=0 = unacceptable.
The server picks the best match and indicates it in Content-Type:
Content-Type: application/json; charset=utf-8
Versioning via Accept header (content negotiation approach to API versioning):
Accept: application/vnd.myapi.v2+json
Vs. URI versioning (/v2/orders) — both are valid; URI versioning is more common because it's simpler to route, log, and cache.
The most ambitious REST constraint. Responses include links to related resources and available actions:
{
"id": 42,
"status": "pending",
"total": 199.98,
"_links": {
"self": { "href": "/orders/42", "method": "GET" },
"cancel": { "href": "/orders/42/cancel", "method": "POST" },
"payment": { "href": "/orders/42/payment", "method": "GET" },
"user": { "href": "/users/7", "method": "GET" }
}
}Intent: clients navigate the API by following links in responses, never hardcoding URLs. The server can change URLs without breaking clients.
Reality: HATEOAS is rarely implemented in practice. Most teams document their API URLs in OpenAPI/Swagger and let clients hardcode them. The cognitive overhead of truly link-driven clients outweighs the flexibility benefit for most applications.
OpenAPI (formerly Swagger) is the standard schema language for describing REST APIs:
openapi: "3.1.0"
info:
title: Orders API
version: "1.0"
paths:
/orders/{id}:
get:
summary: Get an order
parameters:
- name: id
in: path
required: true
schema: { type: integer }
responses:
"200":
description: Order found
content:
application/json:
schema: { $ref: "#/components/schemas/Order" }
"404":
description: Order not foundOpenAPI enables:
- Auto-generated documentation (Swagger UI, Redoc)
- Client SDK generation (OpenAPI Generator)
- Server stub generation
- Request/response validation in tests
- Mock server generation for frontend development
A useful framework for assessing how "RESTful" an API is:
| Level | Name | Description | Example |
|---|---|---|---|
| 0 | The Swamp of POX | Single URI, POST for everything | POST /api {"action":"getOrder","id":42} |
| 1 | Resources | Multiple URIs, but methods used wrong | POST /orders/42/get |
| 2 | HTTP Verbs | Proper use of methods + status codes | GET /orders/42 → 200 |
| 3 | Hypermedia Controls | HATEOAS — links in responses | Level 2 + _links |
Most production APIs are level 2. Level 3 is academically interesting but rarely implemented.
Consistent error responses are as important as success responses. Clients must parse errors to recover or display messages.
{
"error": {
"code": "ORDER_NOT_FOUND",
"message": "Order 42 does not exist or has been deleted.",
"details": [
{
"field": "id",
"issue": "not_found",
"value": "42"
}
],
"documentation_url": "https://docs.example.com/errors/ORDER_NOT_FOUND",
"trace_id": "req_abc123xyz",
"timestamp": "2024-01-15T10:30:00Z"
}
}code— machine-readable error identifier. Clients switch on this.message— human-readable description. For developers, not end users.details— per-field validation errors. Enables form-level error display.trace_id— ties the error to a server-side log entry. Essential for debugging.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request body validation failed.",
"details": [
{ "field": "items[0].quantity", "issue": "must_be_positive", "value": -1 },
{ "field": "shippingAddress.zip", "issue": "invalid_format", "value": "not-a-zip" }
]
}
}Browsers enforce the Same-Origin Policy: JavaScript on app.example.com cannot make fetch() calls to api.other.com without explicit permission.
CORS is the mechanism for granting that permission:
sequenceDiagram
participant B as Browser (app.example.com)
participant API as api.other.com
Note over B,API: Preflight for non-simple requests (POST, custom headers)
B->>API: OPTIONS /api/orders\nOrigin: https://app.example.com\nAccess-Control-Request-Method: POST\nAccess-Control-Request-Headers: Authorization
API-->>B: 204 No Content\nAccess-Control-Allow-Origin: https://app.example.com\nAccess-Control-Allow-Methods: GET, POST, PUT, DELETE\nAccess-Control-Allow-Headers: Authorization, Content-Type\nAccess-Control-Max-Age: 86400
Note over B,API: Actual request (cached preflight result for 24h)
B->>API: POST /api/orders\nOrigin: https://app.example.com\nAuthorization: Bearer ...
API-->>B: 201 Created\nAccess-Control-Allow-Origin: https://app.example.com
Simple requests (GET with standard headers, no custom headers) don't require a preflight. Everything else triggers a preflight OPTIONS request.
Access-Control-Allow-Origin: * allows any origin but disables sending credentials (cookies, Authorization headers). For authenticated APIs, use the specific origin: Access-Control-Allow-Origin: https://app.example.com.
Mistake 1: Using GET for state-changing operations
GET /orders/42/cancel — GET must be safe (no side effects). Use POST /orders/42/cancel.
Mistake 2: 200 OK for errors
HTTP/1.1 200 OK
{"success": false, "error": "Order not found"}HTTP clients (middleware, CDNs, monitoring) use status codes for routing and alerting. A 200 for an error confuses every system that processes it.
Mistake 3: Ignoring idempotency for payment/booking endpoints
Always implement Idempotency-Key for operations that must not be duplicated.
Mistake 4: Returning 404 vs 403 for auth errors If the resource exists but the user can't see it, return 404 (not 403) to prevent leaking resource existence to unauthorised callers. This is a security/privacy decision, not a technical one.