REST API Fundamentals
REST is an architectural style, not a JSON convention. The useful core for application APIs is:
- Resources are nouns identified by URIs.
- Representations (JSON) are transferred, not the resources themselves.
- Uniform interface: standard methods and status codes.
- Stateless requests: each request carries the context it needs.
- Cacheable responses where semantics allow.
Most "REST vs RPC" debates are really about resource modelling and cacheability, not purity.
Modelling resources
Name things, not actions:
| Good | Avoid |
|---|---|
GET /orders |
GET /getOrders |
POST /orders |
POST /createOrder |
GET /orders/{id} |
GET /order?id=... |
POST /orders/{id}/cancel |
POST /orders/{id}/actions/cancel (acceptable for actions) |
Actions that are not CRUD can be modelled as sub-resources or explicit action endpoints:
POST /orders/{id}/cancel
POST /payments/{id}/capture
POST /users/{id}/verify-email
Pragmatic REST accepts action endpoints when forcing them into PATCH status would hide semantics.
Hierarchy and depth
/customers/{customerId}/orders
/customers/{customerId}/orders/{orderId}/items
Two levels are readable; five levels become a query language. For deep relationships, prefer top-level resources with filters:
GET /orders?customerId=c_1&status=open
Identifiers
- Opaque, stable, URL-safe (UUID/ULID/nanoid).
- Do not expose sequential integers if enumeration is a risk.
- Allow client-generated ids for idempotent creates where useful (
PUT /orders/{clientId}).
Representations
- JSON by default; be explicit about
Content-Type. - Use a consistent envelope or none at all — but be consistent:
{ "data": { ... }, "meta": { ... } }
- Never change a field's type or meaning within a version.
- Tolerate unknown fields on input to allow additive evolution.
- Use ISO-8601 UTC timestamps and integer cents (or decimal strings) for money.
Collections
| Need | Convention |
|---|---|
| Filter | ?status=open&customerId=c_1 |
| Sort | ?sort=-createdAt,id (document allowed fields) |
| Paginate | ?limit=50&cursor=... |
| Search | ?q=... |
| Fields | ?fields=id,status,total (sparse fieldsets) |
| Expand | ?expand=customer (avoid unbounded nesting) |
Pagination: prefer cursor/keyset over offset.
GET /orders?limit=50&cursor=eyJjcmVhdGVkQXQiOi...
{
"data": [ ... ],
"nextCursor": "eyJjcmVhdGVkQXQiOi...",
"hasMore": true
}
Offset pagination slows down and skips/duplicates rows under writes. If you must support page numbers, cap the offset.
Status codes, again
| Operation | Success | Notes |
|---|---|---|
| List | 200 |
paginated |
| Get | 200 / 304 |
conditional GET |
| Create | 201 + Location |
or 202 if async |
| Replace | 200/204 |
412 on If-Match mismatch |
| Patch | 200/204 |
document merge semantics |
| Delete | 204/202 |
idempotent: second delete → 204/404 consistently |
| Validation | 400/422 |
one convention, documented |
| Conflict | 409 |
duplicate key, state conflict |
| Auth | 401/403 |
never conflate |
| Rate limit | 429 + Retry-After |
Errors
{
"error": {
"code": "order_already_shipped",
"message": "Order ord_123 has shipped and cannot be cancelled.",
"requestId": "req_01H...",
"details": { "orderId": "ord_123", "status": "shipped" }
}
}
codeis stable and machine-readable.messageis for humans and may change.- Field-level validation errors belong in
details. - Do not leak stack traces or SQL.
Concurrency control
Two clients edit the same resource. Options:
- Optimistic (preferred for APIs):
ETag+If-Match.
GET /orders/ord_1
ETag: "v7"
PATCH /orders/ord_1
If-Match: "v7"
Mismatch → 412 Precondition Failed; the client re-reads and retries.
- Version field in the body (
version: 7), same semantics with409. - Pessimistic locks (
POST /orders/{id}/lock) — avoid for public APIs; they do not compose with retries.
Batch and bulk operations
- Prefer a small number of explicit endpoints over one generic
POST /batch. - Bulk creates should be idempotent per item and return per-item results, not one opaque failure:
{
"results": [
{ "index": 0, "status": 201, "id": "ord_1" },
{ "index": 1, "status": 409, "error": { "code": "duplicate" } }
]
}
- Cap batch size; large imports belong in an async job with a status endpoint.
HATEOAS: the pragmatic take
Hypermedia links (_links) help discoverability and decouple clients from URL construction:
{
"id": "ord_1",
"status": "open",
"_links": {
"self": "/orders/ord_1",
"cancel": "/orders/ord_1/cancel",
"items": "/orders/ord_1/items"
}
}
Most teams use links only for pagination and actions. Full HATEOAS (clients navigating solely by links) is rare outside mature public APIs; do not force it.
REST anti-patterns
- Verbs in paths for CRUD (
/getUser). - Returning different shapes for the same resource in different endpoints.
200with an error body.- Unbounded collections.
- Deep nesting that mirrors the database schema.
- Chatty APIs that force N+1 calls for one screen.
- Using
POSTfor reads because it is "easier". - Breaking changes without versioning.
When REST is the wrong choice
- Highly connected, client-shaped reads → GraphQL.
- Low-latency service-to-service with streaming → gRPC.
- Server-push updates → SSE/WebSockets.
- Event fan-out to many consumers → webhooks/queues, not polling REST.
Interview drill
1. "Design the API for a booking system."
- Cover: resources (properties, availability, bookings); hold → confirm; idempotency; concurrency control; hold expiry; pagination.
- Follow-up: "How do you prevent double booking?" (DB constraint + version/lock).
- Common miss: offset pagination for availability and no hold semantics.
2. "How do you prevent double booking?"
- Cover: unique/exclusion constraint or conditional update; optimistic version; row lock; hold TTL; idempotency key.
- Follow-up: "What if the payment fails after the hold?" (expiry/release, state machine).
- Common miss: a check-then-insert in application code only.
3. "How do you evolve this API?"
- Cover: additive changes; versioning strategy; deprecation headers/telemetry; contract tests; migration guide.
- Follow-up: "Is adding an enum value breaking?" (tolerant clients vs strict ones).
- Common miss: silent breaking changes and no usage telemetry.
Checklist
- Resources are nouns; actions are explicit sub-resources.
- Methods and status codes match semantics.
- Collections have filtering, stable sort, and cursor pagination.
- Errors have stable codes and field details.
- Writes define idempotency and concurrency behaviour (
ETag/version). - Bulk operations return per-item outcomes and are bounded.
- Representations evolve additively; no field repurposing.
- The API is cacheable where semantics allow.
Next: GraphQL — client-shaped reads and the operational costs that come with them.