REST: resources and verbs
A library has a catalog of things (books, members, loans) and a small set of actions you can take on any of them: look it up, add one, change one, remove one. You don’t need a special desk for “renew loan 42”; you change the loan.
A REST APIREST APIA web API where things are resources at URLs, and HTTP methods like GET and POST say what to do with them.Open in glossary → works the same way. URLs name resources (/orders, /orders/42), and the HTTP method says what to do with them. Because every REST API uses the same verbs and status codes, developers can guess how yours works.
REST’s value in practice is uniformity: HTTP method semantics (safe, idempotent), status codes, caching and intermediaries (CDNs, proxies) all work because the API follows them. The design decisions that matter: resource boundaries, idempotency of writes, pagination, error shape, partial updates, and how state transitions that aren’t plain CRUD (“cancel an order”) are modeled. The implementation side is in Building a REST API.
GET /orders?status=open&limit=20 → 200, a page of orders
GET /orders/42 → 200 the order, or 404
POST /orders → 201 Created, Location: /orders/42
PATCH /orders/42 { "note": "…" } → 200, partially updated
DELETE /orders/42 → 204 No Content
POST /orders/42/cancellation → 202 Accepted (an action as a resource)
| Method | Changes data? | Safe to repeat (idempotent)? | Example |
|---|---|---|---|
GET | No, never | Yes | Read an order |
PUT | Yes, replaces | Yes: same result every time | Replace the shipping address |
DELETE | Yes | Yes: deleting twice still leaves it deleted | Remove a saved card |
PATCH | Yes, partly | Depends (“set note” yes, “add 1” no) | Change one field |
POST | Yes, creates | No: twice creates two | Place an order |
Tap the line with the bug.
# Order screen links
GET /orders/42 → show the order
GET /orders/42/cancel → cancel it (used by the "Cancel order" link in emails)
GET /orders/42/invoice → download the invoice PDFHint: Email clients scan links for malware, browsers prefetch links, and HTTP libraries retry GET requests automatically. What do they all assume about GET?
GET is defined as safe: it must never change anything. Everything in the HTTP world relies on that: email security scanners open every link in an email to check it, browsers and apps prefetch links, caches store GET responses, and HTTP clients retry GETs on timeouts. A cancel link that works over GET gets clicked by a scanner the moment the email arrives, and customers find their orders cancelled before they read the email.
State changes use methods that say so, and links in emails lead to a page with a confirm button:
GET /orders/42/cancel → a page: "Cancel order 42?" [Confirm]
POST /orders/42/cancellation → actually cancels; 202 Accepted (or 409 if already shipped)Creating things safely
Phones lose connection in elevators and tunnels. When the app sends POST /orders and the answer never arrives, it can’t know whether the order was created. Retrying might create a second order; not retrying might mean no order. The fix is an idempotency key: the app generates a unique id for this checkout attempt and sends it with every retry. The server remembers keys it has seen and returns the original result instead of creating a duplicate.
let key = UUID().uuidString // one key per checkout attempt, reused on retries
var request = URLRequest(url: URL(string: "https://api.shop.example/orders")!)
request.httpMethod = "POST"
request.setValue(key, forHTTPHeaderField: "Idempotency-Key")
request.httpBody = try JSONEncoder().encode(cart)
let (data, _) = try await session.data(for: request) // safe to retry with the same keyval key = UUID.randomUUID().toString() // one key per checkout attempt, reused on retries
val response = client.post("https://api.shop.example/orders") {
header("Idempotency-Key", key)
contentType(ContentType.Application.Json)
setBody(cart)
} // safe to retry with the same key// Server side (Spring): remember the key with the result, in the same transaction as the order
@PostMapping("/orders")
ResponseEntity<OrderDto> create(@RequestHeader("Idempotency-Key") String key, @RequestBody CartDto cart) {
return idempotency.findResult(key)
.map(saved -> ResponseEntity.status(201).body(saved)) // a retry: same answer
.orElseGet(() -> ResponseEntity.status(201).body(orders.placeAndRemember(key, cart)));
}const key = crypto.randomUUID(); // one key per checkout attempt, reused on retries
const response = await fetch("https://api.shop.example/orders", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(cart),
}); // safe to retry with the same keyPredict the output
The app sends POST /orders without an idempotency key. The server creates order 1001, but the response is lost in a tunnel. The app retries automatically. How many orders exist?
The server can't tell the retry from a new order, so it creates order 1002: the customer is charged twice. With an Idempotency-Key header, the server would find the key from the first attempt and return order 1001 again. Payment APIs like Stripe made this pattern standard; every non-idempotent write a mobile app can retry should have one.
Design a notes API
A notes app needs to: list a user’s notes (newest first, possibly thousands), read one note, create a note, edit a note’s title or text, pin and unpin a note, and delete a note. Write the endpoints (method, path, main status codes), and say which writes need an idempotency key.
›
Pinning is a change to one field of a note. Thousands of notes means pagination: which kind?
SolutionTry it yourself first
GET /notes?limit=50&cursor=… → 200 { items: [...], nextCursor: "…" } newest first
GET /notes/{id} → 200, or 404
POST /notes → 201 Created + Location (Idempotency-Key)
PATCH /notes/{id} → 200 { "title": "…" } or { "text": "…" }
PATCH /notes/{id} { "pinned": true } → 200 (setting a value: idempotent)
DELETE /notes/{id} → 204 (again: 204 or 404, both mean "gone")- Cursor pagination (
nextCursor), so new notes don’t shift pages and deep pages stay fast. - Only
POSTneeds an idempotency key:PATCHwith “set pinned to true” andDELETEare already safe to repeat. - Pinning isn’t its own endpoint: it’s a field. A separate
/pinaction would be fine too, as long as it usesPUT/POST, neverGET. - Edit conflicts (two devices editing the same note) can be caught with
ETag+If-Match: the server answers412 Precondition Failedif the note changed since the client loaded it.
REST details that matter at scale
- Errors: one shape for all errors (RFC 9457
application/problem+json:type,title,status,detail, plus fields), with machine-readable codes the client can branch on, not English sentences. - Idempotency keys: store key → (request hash, response) with a TTL, scoped per client; reject the same key with a different body (
422); hold a lock while the first request is in flight so concurrent retries don’t both execute. - Concurrency:
ETag+If-Matchfor optimistic updates;409for state conflicts (“can’t cancel a shipped order”). - Rate limits and retries:
429withRetry-After; document which errors are retryable. - Partial responses:
fields=or separate lightweight list representations, so mobile list screens don’t download full detail objects.
- Method semantics are load-bearing: GET safe (scanners, prefetch, caches, retries rely on it); PUT/DELETE idempotent; POST needs an
Idempotency-Key(stored with the response, request-hash checked, locked while in flight). - Model actions as resources or POSTs (
/cancellation), never as GETs. - Cursor pagination,
problem+jsonerrors with stable codes,ETag/If-Matchfor concurrency,409/412/429withRetry-After.
Check this part
Why must a GET request never change data?
Which request can create a duplicate if it's retried?
A client reuses an idempotency key with a different request body. What should the server do?