REST API Design
REST (Representational State Transfer) is an architectural style, not a protocol. If you’re building or reviewing APIs at mid-senior level, interviewers expect you to reason about trade-offs, not just recite definitions.
REST Constraints
Section titled “REST Constraints”| Constraint | What it means |
|---|---|
| Client-Server | UI and data storage are decoupled — each evolves independently |
| Stateless | Every request carries all the context it needs — no server-side session |
| Cacheable | Responses must declare if they’re cacheable to improve performance |
| Uniform Interface | Resources are identified by URIs; representations are separate from resources |
| Layered System | Client can’t tell if it’s talking to the origin server or a proxy/cache |
| Code on Demand | (Optional) Server can send executable code (e.g. JavaScript) to the client |
HTTP Methods & Idempotency
Section titled “HTTP Methods & Idempotency”| Method | Safe? | Idempotent? | Use case |
|---|---|---|---|
GET | ✅ | ✅ | Fetch a resource |
POST | ❌ | ❌ | Create a resource |
PUT | ❌ | ✅ | Replace a resource entirely |
PATCH | ❌ | ⚠️ Depends | Partial update |
DELETE | ❌ | ✅ | Remove a resource |
Safe = no side effects. Idempotent = same result no matter how many times you call it.
PUT /users/123 with the same body always produces the same state — idempotent. POST /orders creates a new order each time — not idempotent.
Idempotency Keys for POST
Section titled “Idempotency Keys for POST”If a POST fails mid-flight (e.g. payment), can you retry safely? Not by default. The fix: include an idempotency key in the request header.
POST /paymentsIdempotency-Key: a8f3c2d1-4b72-4e6a-9f10-123456789abc
{ "amount": 5000, "currency": "GBP" }The server stores the key and returns the cached response on retry. Same request, same outcome — safe to retry without double-charging.
Resource Naming
Section titled “Resource Naming”Rules:
- Use nouns, not verbs — the HTTP method is the verb
- Use plural for collections
- Use nested paths for relationships
- Use kebab-case for multi-word resources
| ❌ Wrong | ✅ Right |
|---|---|
GET /getUser | GET /users/123 |
POST /createOrder | POST /orders |
DELETE /deleteAccount/5 | DELETE /accounts/5 |
GET /user/123/getOrders | GET /users/123/orders |
GET /UserProfile | GET /user-profiles/123 |
Nesting beyond 2 levels (/users/123/orders/456) is fine. Going deeper than that usually signals you need a flatter design.
Versioning
Section titled “Versioning”| Strategy | Example | When to use |
|---|---|---|
| URI path | /v1/users | Default choice — visible, easy to test, cache-friendly |
| Query param | /users?version=1 | Avoid — messy, not RESTful |
| Header | Accept: application/vnd.api.v1+json | Clean but harder to test in browser/curl |
| Subdomain | v1.api.example.com | Large orgs with separate infra per version |
Breaking changes require a new version. Adding a field is safe. Removing, renaming, or changing a field’s type is a breaking change.
Error Handling
Section titled “Error Handling”| Status | Meaning | When to use |
|---|---|---|
200 OK | Success | GET, PUT, PATCH returned data |
201 Created | Resource created | POST that creates a new resource |
400 Bad Request | Invalid input | Malformed JSON, missing required field |
401 Unauthorized | Not authenticated | Missing or invalid token |
403 Forbidden | Not authorised | Valid token, but no permission |
404 Not Found | Resource missing | ID doesn’t exist |
409 Conflict | State conflict | Duplicate entry, version mismatch |
422 Unprocessable | Validation failed | Fields present but values invalid |
429 Too Many Requests | Rate limited | Include Retry-After header |
500 Internal Server Error | Server blew up | Never expose stack traces to clients |
Always return a consistent error body:
{ "error": "validation_failed", "message": "Email address is not valid", "field": "email"}REST vs GraphQL
Section titled “REST vs GraphQL”| REST | GraphQL | |
|---|---|---|
| Data shape | Fixed by server | Client defines the query |
| Over-fetching | Common — you get what the endpoint returns | Gone — request only what you need |
| Under-fetching | Multiple roundtrips for related data | Single query, nested relationships |
| Caching | Simple — HTTP caching works natively | Harder — all POST to /graphql |
| Tooling | Universal | Requires GraphQL client (Apollo, urql) |
| Best for | Public APIs, simple CRUD, external consumers | Complex UIs, mobile (bandwidth-sensitive), internal APIs |
Use REST when you’re building a public API or simple CRUD. Use GraphQL when a mobile app or complex UI needs to control its own data shape.
Common Gotchas
Section titled “Common Gotchas”- Verbs in URLs —
/getUseris an RPC call disguised as a URL. REST uses nouns; the method is the verb. - POST is not idempotent — on network retry, you may create duplicates. Use idempotency keys for anything stateful.
- 401 ≠ 403 — authentication failure vs authorisation failure; mixing these up signals a shallow understanding.
- Adding a field is safe; removing one is a breaking change — bump the version before removing or renaming anything consumers depend on.
- REST over-fetching leads to GraphQL — knowing when to swap is the mature answer, not defending REST in all cases.