Skip to content

REST APIs

REST is the closest thing the web has to a default. When someone says “we’ll expose an API,” they almost always mean a REST-ish HTTP API unless they say otherwise. To understand why it won, you have to see it not as a technology but as a set of constraints that happen to align perfectly with how the web already works. REST didn’t add machinery to the web; it described the web’s own grain and said go with it.

The core idea: resources and a fixed set of verbs

Section titled “The core idea: resources and a fixed set of verbs”

A traditional API exposes actions: createUser(), deactivateUser(), getUserOrders(). Each new capability is a new verb, and the vocabulary grows without bound. REST inverts this. It says: model your domain as nounsresources — each with a stable address (a URL), and then act on them with a small, fixed set of verbs that HTTP already defines.

Action-oriented (RPC-ish) Resource-oriented (REST)
POST /createUser POST /users
POST /getUser?id=42 GET /users/42
POST /updateUserEmail PATCH /users/42
POST /deleteUser DELETE /users/42
POST /getUserOrders GET /users/42/orders

The verbs never grow; only the noun-space does. This is why REST APIs feel discoverable: once you know the four or five verbs, you can guess most endpoints.

VerbMeaningSafe?Idempotent?
GETread a resourceyesyes
POSTcreate / non-idempotentnono
PUTreplace a resourcenoyes
PATCHpartially updatenono*
DELETEremove a resourcenoyes

*PATCH can be made idempotent depending on how you define the patch.

Idempotency: the property that survives an unreliable network

Section titled “Idempotency: the property that survives an unreliable network”

Safe means “doesn’t change state” (a GET can be repeated freely). Idempotent means “doing it N times has the same effect as doing it once.” This sounds academic until you remember the fallacies of distributed computing: the network will drop responses. A client sends DELETE /users/42, the server deletes the user, and the response gets lost. The client retries. Because DELETE is idempotent, the second call is harmless — the user is already gone, you return the same result.

Now consider POST /payments. It is not idempotent. A lost response and a blind retry could charge the customer twice. This is the single most important practical lesson of REST design:

Statelessness: each request carries its own context

Section titled “Statelessness: each request carries its own context”

REST is stateless: the server keeps no per-client conversational memory between requests. Every request must carry everything needed to serve it — auth token, parameters, body. The server doesn’t remember that “this is the same client who logged in two requests ago” unless that fact travels in the request (e.g., in a token).

What does this buy us, and what does it cost? It buys horizontal scalability: because no server holds client-specific state, any replica can handle any request, so a load balancer can spray traffic across a fleet freely (see statelessness & sessions). It costs redundancy on the wire — every request re-sends auth and context — and it pushes session state somewhere else (a token, a cache, a database).

Status codes: a shared vocabulary for outcomes

Section titled “Status codes: a shared vocabulary for outcomes”

HTTP gives you a standard way to say what happened, grouped by leading digit:

2xx success 200 OK, 201 Created, 204 No Content
3xx redirection 301 Moved Permanently, 304 Not Modified
4xx client's fault 400 Bad Request, 401 Unauthorized, 404 Not Found, 409 Conflict
5xx server's fault 500 Internal Error, 503 Service Unavailable

The discipline that separates a good API from a frustrating one: 4xx means the caller must change something; 5xx means the caller may retry later. Returning 200 OK with an error message buried in the body breaks every monitoring tool and retry policy that relies on the status code. Be honest with the number.

Versioning: evolving without breaking callers

Section titled “Versioning: evolving without breaking callers”

Once clients depend on your API, you can’t freely change it — a renamed field breaks them. So you version. Two common styles:

  • URL versioning: /v1/users, /v2/users. Crude but unmissable and easy to route.
  • Header versioning: Accept: application/vnd.myapp.v2+json. Cleaner URLs, but invisible and easier to get wrong.

The honest trade-off: versioning buys you the freedom to evolve, at the cost of maintaining multiple versions in parallel — every version you ship is one you must keep alive until its last caller leaves.

Pagination: never return an unbounded list

Section titled “Pagination: never return an unbounded list”

GET /events on a table with ten million rows is a denial-of-service vector against yourself. You must paginate. Two families:

  • Offset/limit (?offset=40&limit=20): simple, lets you jump to any page — but slow on deep pages (the database still scans past skipped rows) and unstable if rows are inserted while paging.
  • Cursor/keyset (?after=<opaque_cursor>): you pass back a pointer to the last item seen. Fast and stable under inserts, but you can’t jump to “page 500” directly.

Cursor pagination wins for large, mutating datasets precisely because it leans on an index instead of counting from the start.

REST is the right default, but it is not free of friction:

  • Over- and under-fetching. A fixed resource shape rarely matches every screen. A mobile view wants three fields; the endpoint returns thirty. Or it needs data from three resources and must make three round trips. This exact pain is what GraphQL attacks.
  • Chattiness. Resource-orientation can mean many small calls where one custom call would do.
  • Weak typing across the wire. JSON has no enforced schema by default; the contract lives in documentation, not in the compiler — which is part of the appeal of RPC/gRPC for internal services.

How do two services agree on what an operation means? REST answers: agree on a small, universal set of verbs and let the nouns carry the meaning. By riding HTTP’s existing semantics — methods, status codes, statelessness — REST gets caching, scalability, and tooling almost for free. You pay in rigidity of shape and chattiness, which is exactly why the next chapters exist.

  1. Why does a resource-oriented design keep the verb vocabulary small while an action-oriented one grows it without bound?
  2. Distinguish safe from idempotent, and explain why idempotency matters specifically because the network is unreliable.
  3. How does an idempotency key make a POST /payments safe to retry?
  4. Why does statelessness enable horizontal scaling, and what cost does it impose?
  5. When would you choose cursor pagination over offset pagination, and what capability do you give up by doing so?