Handbooks / REST API Design / Chapter 2
Resource Design
7 pages · ~5 min✓ Reviewed
Builds on HTTP Methods and Status Codes. Next up: Authentication and Authorization.
REST Mental Model and Constraints
1. Resources
- Everything is a resource (a noun) at a URI
- Clients exchange representations (usually JSON) of its state
2. Six Constraints
- Client-server: UI and data split
- Stateless: each request carries all context (token, params)
- Cacheable: responses declare it
- Uniform interface: standard verbs and URIs
- Layered system: proxies, gateways
- Code-on-demand: optional
Stateless
No server session between calls: any server can answer, so you scale horizontally.
3. Methods
| Method | Use | Safe | Idempotent |
|---|---|---|---|
GET | read | yes | yes |
POST | create | no | no |
PUT | replace | no | yes |
PATCH | partial update | no | not guaranteed |
DELETE | remove | no | yes |
- Of these five, only GET is safe (HEAD and OPTIONS are safe too)
4. Interview Point
- RESTful vs just HTTP? Resources plus all six constraints
- Why stateless? Any server serves any request, so it scales out
- Safe vs idempotent? Safe: GET. Idempotent: GET, PUT, DELETE
Resource Naming, Nesting and Status Codes
1. Naming Rules
- Plural nouns, lowercase kebab-case
- Never verbs: the HTTP method is the verb
| Avoid | Use |
|---|---|
/getOrder?id=42 | GET /orders/42 |
/createUser | POST /users |
/OrderItems | /order-items |
2. Nesting
- Nest only to show ownership, 1-2 levels deep
- Item has a unique ID? Expose it at top level:
/orders/42 - Keep the nested path for listing:
/users/7/orders
| Path | Verdict |
|---|---|
/users/7/orders | OK, ownership |
/users/7/orders/42/items/3/reviews | Too deep |
/items/3/reviews | Flattened |
3. Actions and Create
- Non-CRUD action: model a noun sub-resource, not a verb
- Cancel an order:
POST /orders/42/cancellation
http
POST /orders HTTP/1.1 201 Created Location: /orders/43
Create response
POST returns 201 Created plus a Location header pointing at the new resource.
Filtering, Sorting, Bodies and Error Formats
1. Query Params on the Collection
http
GET /orders?status=paid&sort=-created_at&limit=20&cursor=abca - prefix means descending
- Filter, sort and paginate with query params
- Cursor pagination for large or changing data
- Offset slows down and skips or duplicates rows as data shifts
2. Bodies and Status Codes
- Send JSON with
Content-Type: application/json - PUT needs the full object; PATCH sends only changed fields
- Return the created or updated resource
| Code | Meaning |
|---|---|
| 200 / 201 / 204 | OK / Created / No Content |
| 400 / 422 | bad input / validation failed |
| 401 / 403 | unauthenticated / forbidden |
| 404 / 409 | not found / conflict |
| 429 / 500 | rate limited / server error |
3. Error Body
json
{ "type": "...",
"title": "Validation failed",
"status": 422,
"detail": "email is invalid",
"errors": [{ "field": "email" }] }RFC 9457 problem+json shape
Same shape every time
401 = not logged in. 403 = logged in but not allowed.
HATEOAS and REST vs GraphQL vs gRPC
1. HATEOAS
- Responses carry links to the next valid actions
- Clients follow links instead of hardcoding URLs
- Most real APIs skip it
json
{ "id": 42, "status": "pending",
"_links": {
"pay": "/orders/42/payment",
"cancel": "/orders/42" } }the server decides which actions are open
2. REST vs GraphQL vs gRPC
| REST | GraphQL | gRPC | |
|---|---|---|---|
| Shape | many URLs, HTTP + JSON | one endpoint, client picks fields | Protobuf over HTTP/2, strict contract |
| Best for | public APIs | varied clients, e.g. mobile | internal service-to-service |
| Strength | easy HTTP caching | fixes over-fetching | streaming, small payloads |
| Risk | over- and under-fetching | harder caching; needs depth or complexity limits | browsers need gRPC-Web; hard to debug by eye |
3. Interview Point
- What does HATEOAS solve? Hardcoded URLs: links tell clients the next valid actions
- GraphQL over REST? Varied clients that need exact fields
- gRPC over REST? Fast internal calls with a strict contract
- Why is GraphQL caching harder? One endpoint, so URL-keyed HTTP caches cannot tell queries apart
Common REST Design Mistakes
1. Mistakes and Fixes
| Mistake | Fix |
|---|---|
POST /createUser | POST /users |
GET /deleteOrder?id=42 | DELETE /orders/42 |
200 + error in body | 4xx + problem+json |
500 for bad input | 400 / 422 |
Unbounded GET /orders | /v1/orders?limit=20 |
- GET that changes state breaks the safe contract
- Crawlers and prefetchers fire it: side effects
- 200 + error hides failures from clients and monitoring
2. Version and Paginate Early
- Add
/v1or a version header from day one - Put a limit on every collection, with a max cap
- Unbounded lists break later: slow, huge payloads
http
GET /v1/orders?limit=20&cursor=abcversion + limit + cursor
3. Summary and Interview Points
Status code is the contract
Noun URL + right method + honest status: 4xx = client fault, 5xx = server fault.
- Version a public API?
/v1in path or a header; keep old versions alive - No pagination? Huge responses, timeouts, memory pressure as data grows
- Errors as 200? Clients, caches and monitoring read only the status code
REST API Cheat Sheet
1. Methods and URLs
| Method | Use | Idempotent |
|---|---|---|
GET | read | yes |
POST | create | no |
PUT | replace | yes |
PATCH | partial update | not guaranteed |
DELETE | remove | yes |
- Plural nouns, kebab-case, nest at most two levels
- Filter, sort and paginate with query params
-prefix on sort means descending
http
GET /orders?status=paid&sort=-created_at&limit=20&cursor=...2. Status Codes
| Group | Codes |
|---|---|
| Success | 200 OK, 201 + Location, 204 No Content |
| Client | 400 input, 401 unauthenticated, 403 forbidden, 404, 409 conflict, 422 validation, 429 rate limited |
| Server | 500 |
3. Pick One
- REST: public CRUD APIs
- GraphQL: flexible client-driven queries
- gRPC: fast internal service calls
- HATEOAS is optional
Errors
Never return 200 with an error body. Keep 500 for server faults only.
Check yourself
A user has name and email. A client sends PUT /users/7 with only {"email": "new@x.com"}. What goes wrong?
- PUT replaces the whole object, so name is lost
- Use
PATCHto send only changed fields
Page 1 of /feed?offset=0&limit=20 is loaded. A new row is then inserted at the top. What does page 2 show?
- The old 20th row shifts to 21st, so it appears twice
- Cursor pagination avoids this
Spot the bug: GET /orders/42/cancel cancels an order. A link prefetcher visits it. Result?
- The order is cancelled, because GET must be safe
- Use
POST /orders/42/cancellation