Skip to content

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.


ConstraintWhat it means
Client-ServerUI and data storage are decoupled — each evolves independently
StatelessEvery request carries all the context it needs — no server-side session
CacheableResponses must declare if they’re cacheable to improve performance
Uniform InterfaceResources are identified by URIs; representations are separate from resources
Layered SystemClient 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

MethodSafe?Idempotent?Use case
GETFetch a resource
POSTCreate a resource
PUTReplace a resource entirely
PATCH⚠️ DependsPartial update
DELETERemove 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.

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 /payments
Idempotency-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.


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 /getUserGET /users/123
POST /createOrderPOST /orders
DELETE /deleteAccount/5DELETE /accounts/5
GET /user/123/getOrdersGET /users/123/orders
GET /UserProfileGET /user-profiles/123

Nesting beyond 2 levels (/users/123/orders/456) is fine. Going deeper than that usually signals you need a flatter design.


StrategyExampleWhen to use
URI path/v1/usersDefault choice — visible, easy to test, cache-friendly
Query param/users?version=1Avoid — messy, not RESTful
HeaderAccept: application/vnd.api.v1+jsonClean but harder to test in browser/curl
Subdomainv1.api.example.comLarge 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.


StatusMeaningWhen to use
200 OKSuccessGET, PUT, PATCH returned data
201 CreatedResource createdPOST that creates a new resource
400 Bad RequestInvalid inputMalformed JSON, missing required field
401 UnauthorizedNot authenticatedMissing or invalid token
403 ForbiddenNot authorisedValid token, but no permission
404 Not FoundResource missingID doesn’t exist
409 ConflictState conflictDuplicate entry, version mismatch
422 UnprocessableValidation failedFields present but values invalid
429 Too Many RequestsRate limitedInclude Retry-After header
500 Internal Server ErrorServer blew upNever expose stack traces to clients

Always return a consistent error body:

{
"error": "validation_failed",
"message": "Email address is not valid",
"field": "email"
}

RESTGraphQL
Data shapeFixed by serverClient defines the query
Over-fetchingCommon — you get what the endpoint returnsGone — request only what you need
Under-fetchingMultiple roundtrips for related dataSingle query, nested relationships
CachingSimple — HTTP caching works nativelyHarder — all POST to /graphql
ToolingUniversalRequires GraphQL client (Apollo, urql)
Best forPublic APIs, simple CRUD, external consumersComplex 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.


  • Verbs in URLs/getUser is 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.