Architectural Styles · Lesson 02
REST architecture style
REST isn't a protocol or a file format — it's a set of six constraints on how components of a distributed system should interact. Get those constraints right and you get an architecture that scales to the entire web.
By the end you'll be able to
- Define REST's key constraints: resources + URIs, uniform interface, statelessness, representations, cacheability.
- Map the four HTTP methods (GET/POST/PUT/DELETE) onto CRUD operations on a resource.
- Spot an RPC-style verb URL and explain why it violates the uniform interface.
REST is a set of constraints, not a spec
Roy Fielding coined the term REST in his 2000 PhD dissertation. He wasn't inventing something new — he was describing the architectural decisions that made the World Wide Web work at global scale. The key insight: the web's longevity and scale come from six architectural constraints. Obey them and you get the same properties for your API.
Think of REST like the rules of contract bridge. The rules don't specify which cards you hold, only how you're allowed to play them. Any valid hand played under those rules is a "REST API"; hands played under different rules (calling a custom action like /calculateTax) are a different game entirely.
Constraint 1 — Resources and URIs
Every piece of information worth addressing is a resource. A resource is a conceptual thing — a user, an order, a document. Every resource gets a stable URI (Uniform Resource Identifier) — the address that never changes regardless of what's inside it.
The URI names what you want, not what you want done to it:
/users/42— the user with id 42 (a resource)/orders/99/items— the item collection belonging to order 99/documents/annual-report-2025— a specific document
Constraint 2 — The uniform interface
All resources are manipulated through the same small set of operations. In HTTP-based REST those operations are the HTTP methods. Their semantics are fixed by the HTTP spec — you don't invent new verbs:
| Method | Meaning | Safe? | Idempotent? |
|---|---|---|---|
GET | Read a resource | Yes | Yes |
POST | Create a new resource (or trigger a process) | No | No |
PUT | Replace a resource entirely | No | Yes |
PATCH | Update part of a resource | No | Conditional |
DELETE | Remove a resource | No | Yes |
Safe means the operation has no observable side-effect (you can call it freely). Idempotent means calling it once or a hundred times produces the same result — crucial for retries.
Constraint 3 — Statelessness
Each request must carry all the context the server needs to service it. The server keeps no session state between calls. Your phone knows what song it just played; a REST server does not know what the client called last time.
This makes REST servers easy to scale: any instance can handle any request because none of them hold per-client state. The trade-off is that credentials and context travel in every request (usually via headers), making requests slightly larger.
Constraint 4 — Representations
A resource and its representation are different things. /users/42 is the resource; the JSON object you get back is a representation of it at this moment. The same resource could have many representations — JSON for an app, HTML for a browser, CSV for a data export. HTTP's Accept header lets clients negotiate which one they want (content negotiation).
Accept header; the server sends back a Content-Type confirming what it returned.Constraint 5 — Cacheability
Responses must label themselves as cacheable or non-cacheable. When a response is cacheable, clients, proxies, and CDNs can store it and reuse it — eliminating round-trips to the origin entirely. The HTTP spec's cache-control vocabulary (Cache-Control, ETag, Last-Modified) is the mechanism. A GET /products/123 response with Cache-Control: max-age=300 can be served from a CDN edge for 5 minutes without touching the origin server at all.
Constraint 6 — HATEOAS (briefly)
Hypermedia As The Engine Of Application State is the most frequently skipped REST constraint. The idea: responses include links that tell the client what it can do next, so clients don't need to hardcode URL templates. A GET /orders/99 response might embed "links": [{"rel":"cancel","href":"/orders/99/cancel"}]. The client navigates the API by following links, like a browser following anchor tags — no prior knowledge of URL structure required.
Full HATEOAS is rarely implemented in practice but the principle (clients shouldn't hardcode internal URL structure) remains a useful design guide.
Worked example: /users/42 with all four main methods
# Read the user
GET /users/42
# → 200 OK
{ "id": 42, "name": "Kwame Asante", "email": "k@example.com" }
# Create a new user (collection resource)
POST /users
{ "name": "Linnea Berg", "email": "l@example.com" }
# → 201 Created Location: /users/43
# Replace the user entirely
PUT /users/42
{ "name": "Kwame A. Asante", "email": "k@example.com" }
# → 200 OK (or 204 No Content)
# Remove the user
DELETE /users/42
# → 204 No Content
Interviewers often ask "what makes an API RESTful?" The trap answer is "it uses HTTP and JSON." The correct answer is: it follows REST's constraints — resources identified by URIs, stateless requests, a uniform interface (HTTP verbs), and cacheable responses. Extra credit: mention that most "REST" APIs in production are actually REST-ish (they skip HATEOAS) and that's fine in practice.
The uniform interface means the verb lives in the HTTP method, not the URL. You'll often see APIs that put the action in the path:
Verb URLs lock the client to a specific action name; if you later rename the operation you break every client. REST URLs name things, HTTP methods name operations.
Do lean on HTTP's existing semantics: return 201 Created with a Location header after a successful POST; return 204 No Content after a DELETE; use ETag on GET responses so clients can cache. Don't return 200 OK with a body of {"error": "not found"} — that defeats the entire uniform interface.
Under the hood: the full resource lifecycle
A resource moves through create → read → conditional-read → update → delete; each step has specific HTTP semantics that clients and intermediaries rely on. Walking through the full lifecycle on the wire reveals how the pieces — status codes, ETag headers, conditional requests, and HATEOAS links — compose into a coherent system.
Step 1 — Create: POST → 201 Created + Location
A 201 Created response tells the client that a new resource now exists. The Location header carries the canonical URL of that new resource, so the client knows exactly where to find it without issuing a follow-up GET. The server also returns an initial ETag — the fingerprint of the resource as it was just created — so the client can start conditional requests immediately.
# Client sends a new user payload
POST /users HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
Content-Type: application/json
Content-Length: 51
{ "name": "Linnea Berg", "email": "linnea@example.com" }
← HTTP/1.1 201 Created
Location: https://api.example.com/users/43
Content-Type: application/json
ETag: "v1-abc123"
{ "id": 43, "name": "Linnea Berg", "email": "linnea@example.com" }
Step 2 — Read: GET → 200 + ETag
The server issues an ETag — a fingerprint of the current representation — alongside every cacheable GET response. The Cache-Control: max-age=60 header tells intermediaries they can serve this response for up to 60 seconds before re-validating.
GET /users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
← HTTP/1.1 200 OK
Content-Type: application/json
ETag: "v1-abc123"
Cache-Control: max-age=60
{ "id": 43, "name": "Linnea Berg", "email": "linnea@example.com" }
Step 3 — Conditional GET: If-None-Match → 304 Not Modified
The client sends back the ETag it received in the previous response via If-None-Match. If the resource hasn't changed, the server returns 304 Not Modified with no body — saving bandwidth and reducing latency. This is HTTP's built-in optimistic cache validation: the server only does the work of serializing a response body when the data has actually changed.
GET /users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
If-None-Match: "v1-abc123" ← client sends its cached ETag
← HTTP/1.1 304 Not Modified
ETag: "v1-abc123"
← no body sent; client uses its cached copy
Bandwidth saving is concrete: if the user object is 800 bytes, a 304 response is roughly 120 bytes of headers only. Multiply across a million GET requests per hour and the difference is significant — fewer bytes transferred, lower origin CPU, and faster perceived responses for clients with warm caches.
Step 4 — Partial update: PATCH → 200
PATCH sends only the changed fields, not the full representation. The If-Match header enables optimistic locking: the server applies the update only if the resource is still at the version the client last saw. If another process has already changed the resource (new ETag), the server returns 412 Precondition Failed rather than silently overwriting the other writer's changes.
PATCH /users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
Content-Type: application/json
If-Match: "v1-abc123" ← only update if still this version
{ "email": "linnea.berg@example.com" }
← HTTP/1.1 200 OK
ETag: "v2-def456" ← new ETag because the resource changed
{ "id": 43, "name": "Linnea Berg", "email": "linnea.berg@example.com" }
Step 5 — Full replace: PUT → 200/204
PUT replaces the entire resource. The client must send the complete representation — any fields omitted are treated as absent. Use 200 OK with the updated body, or 204 No Content if the server has nothing meaningful to return.
PUT /users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
Content-Type: application/json
{ "name": "Linnea Berg", "email": "linnea.berg@example.com", "plan": "pro" }
← HTTP/1.1 200 OK (or 204 No Content if no body returned)
Step 6 — Delete: DELETE → 204 No Content
204 No Content means "done; there is nothing to say." No body is sent. A subsequent GET on the same URI should return 404 Not Found — the resource is gone.
DELETE /users/43 HTTP/1.1
Host: api.example.com
Authorization: Bearer tok_s3cr3t
← HTTP/1.1 204 No Content
← empty body; 204 means "done, nothing to say"
# Subsequent GET returns 404:
GET /users/43 → 404 Not Found
HATEOAS response — navigable links in the body
A HATEOAS response includes a _links object that tells the client what operations are available from the current state, keyed by semantic relation (rel). The client follows links rather than constructing URLs from hardcoded templates — just as a browser follows anchor tags without knowing site structure in advance.
# GET /orders/99 — response includes navigable links
{
"id": 99,
"status": "pending",
"total": 249.00,
"_links": {
"self": { "href": "/orders/99", "method": "GET" },
"cancel": { "href": "/orders/99/cancel", "method": "POST" },
"pay": { "href": "/orders/99/payment", "method": "POST" },
"customer":{ "href": "/users/42", "method": "GET" }
}
}
The client discovers the cancel and pay operations from the response itself — no hardcoded URL construction needed. If the server later renames the cancel endpoint, only the href changes in the response; clients that follow links instead of constructing URLs don't break.
The two uses of ETag are easy to confuse. If-None-Match is for cache validation on GET: "give me the resource only if it changed since I last fetched it." If-Match is for optimistic locking on PATCH/PUT: "only apply this update if the version I have is still current." Using them backwards causes either stale-read bugs (serving stale cached data) or lost-update bugs (silently overwriting another writer's changes).
How to debug & inspect it
curl -i (the -i flag includes response headers in stdout) is the primary REST debugging tool. It exposes status codes, Location, ETag, and content-negotiation headers that are invisible in the browser Network tab's simplified view.
In browser DevTools (Network tab):
- Filter by XHR/Fetch. Look at the Status column —
201means something was created (check theLocationresponse header);304means a cached copy was served (bandwidth saved). - Click a response: the Headers tab shows
ETagandCache-Control. IfETagis missing on GETs, conditional caching won't work. - Check the Timing tab for Time To First Byte. A
304should be faster than a200because no body is transmitted. - For POST requests, confirm the response has a
Locationheader pointing to the new resource's URL.
Symptom → cause → fix:
| Symptom | Cause | Fix |
|---|---|---|
POST returns 200 instead of 201 |
Server created the resource but didn't signal it with the correct status code | Return 201 Created with a Location header after successful creation |
| GET returns stale data after a PATCH updated the resource | Cached 200 response; ETag not invalidated or Cache-Control max-age still valid |
Server must update ETag on every mutation; or set Cache-Control: no-cache on mutable resources |
DELETE returns 200 with {"success": true} instead of 204 |
Server returning a body when there is nothing meaningful to say | Return 204 No Content with no body; clients that read HTTP correctly don't need a body to confirm deletion |
PATCH on /users/43 returns 404 |
PATCH method not registered in the router; server may handle only GET/POST/PUT | Register a PATCH handler; or check if an API gateway is stripping the method |
If-Match PATCH returns 412 Precondition Failed unexpectedly |
Another process updated the resource between the client's GET and PATCH; ETag no longer matches | Re-fetch with GET to get the current ETag, apply changes, then retry the PATCH with the new ETag |
304 Not Modified but client still re-renders with empty data |
Client not reading the cached response on 304; treating the empty body as fresh data |
On 304, use the locally cached body — no new data was sent, and that is intentional |
Debug checklist:
- Always use
curl -i(not justcurl) — the-iflag includes response headers in stdout, revealingLocation,ETag, andCache-Control. - After a POST: did you get
201 Createdwith aLocationheader? Verify the Location URL is correct by GETting it immediately. - On repeated GETs: is
ETagpresent in the response? IsIf-None-Matchbeing sent in subsequent requests? - On a
304: confirm the client is using its cached body, not treating the empty response as an error or an empty payload. - On a
412: re-fetch to get the current ETag, update your local copy with the latest state, then retry the mutation.
🧠 Quick check
1. Which HTTP method is both safe AND idempotent?
GET is safe (no side-effects) and idempotent (same result however many times you call it). POST is neither — each call can create a new resource. PUT is idempotent but not safe.
2. A REST server stores session state between requests for each client. Is this valid REST?
Statelessness is one of Fielding's six constraints. The server must not retain client session state between requests. It's this constraint that makes REST services easy to scale horizontally.
3. POST /users/42/deactivate violates REST's uniform interface because:
REST URLs name resources; HTTP methods express operations. The correct design is PATCH /users/42 with a body like {"status": "inactive"}. Putting the action in the URL breaks the uniform interface and creates a proliferation of custom verb endpoints.
✍️ Exercise: redesign an RPC-style API (try before opening)
A legacy system exposes these endpoints. Redesign them to follow REST constraints:
Model answer:
Rubric: ✓ verbs removed from URLs ✓ nouns are plural ✓ correct HTTP method used for each operation ✓ query string kept for filter params (not for ids of single resources).
Key takeaways
- REST is six constraints, not a protocol: client-server, statelessness, cacheability, uniform interface, layered system, code-on-demand.
- Resources are named by URIs; operations are expressed by HTTP methods — never put verbs in URLs.
- Responses return representations of resources — the same resource can have JSON, HTML, and CSV representations.
- Statelessness enables horizontal scaling; every request is self-contained.
- HTTP caching (ETag, Cache-Control) is built into REST for free.