REST and Web APIs

A web page is HTML meant for a human's eyes. But most traffic on the modern web is machines talking to machines: your weather app fetching a forecast, a checkout page asking a payment provider to charge a card, a phone syncing your notes. None of those want a page — they want data. The contract that lets one program call another over the web is a web API (Application Programming Interface), and by far the most common way to shape one is a style called REST.

REST is not a protocol, a library, or a standard you install. It is an architectural style — a set of constraints described by Roy Fielding in his 2000 doctoral thesis, distilled from what made the web itself scale. The good news for you: REST is built directly on the HTTP you already know. Its verbs, its status codes, its statelessness — a REST API is really just HTTP used with discipline. This page is about that discipline: what the constraints are, and why sloppy "REST" that ignores them causes real pain.

Resources and representations

The central noun in REST is the resource: any thing worth naming — a user, an order, a photo, today's weather in Oslo. Every resource gets a URI (a URL) that is its stable address. The design rule is that URIs name things (nouns), not actions (verbs):

/users → the collection of all users /users/42 → one specific user (the resource "user 42") /users/42/orders → the collection of user 42's orders /orders/1007 → one specific order

What actually travels over the wire is not the resource itself but a representation of it — a snapshot in some format, almost always JSON today. The same resource /users/42 might be represented as JSON for an app, or HTML for a browser; the resource is the abstract thing, the representation is one concrete encoding of its current state.

GET /users/42 → 200 OK { "id": 42, "name": "Alice", "email": "[email protected]" }

The uniform interface: CRUD maps onto HTTP methods

Here is REST's most practical idea, the uniform interface. Instead of inventing a new function name for every operation (getUser, createUser, deleteUser, updateUserEmail…), you use the same small set of HTTP methods on every resource. The four basic data operations — Create, Read, Update, Delete (CRUD) — map straight onto methods you already met:

IntentMethod + URITypical success codeIdempotent?
Read a collectionGET /users200 OKyes
Read oneGET /users/42200 OKyes
CreatePOST /users201 Createdno
Replace / updatePUT /users/42200 OKyes
DeleteDELETE /users/42204 No Contentyes

Notice how the design leans on the safety and idempotency rules from HTTP. POST /users creates a new user each time it's called — not idempotent, so it lives at the collection URI and the server assigns the id. PUT /users/42 sets user 42 to a known state — idempotent, so it targets a specific URI and you can safely retry it after a dropped connection. This isn't decoration; it's what makes an API robust on a flaky network.

Build a tiny REST router

The heart of a REST server is a router: it looks at the request's method and path and dispatches to the right action over a data store. That's pure logic — no real network required — so we can run a whole miniature API in the sandbox. The store below is just an in-memory object; watch how the same five requests you saw in the table drive it.

// In-memory "database": a map from id -> user. const db = { 42: { id: 42, name: "Alice" } }; let nextId = 43; // The router: dispatch on (method, path) and return {status, body}. function handle(method, path) { const parts = path.split("/").filter(Boolean); // "/users/42" -> ["users","42"] const isCollection = parts.length === 1 && parts[0] === "users"; const isItem = parts.length === 2 && parts[0] === "users"; const id = isItem ? Number(parts[1]) : null; if (method === "GET" && isCollection) return { status: 200, body: Object.values(db) }; if (method === "GET" && isItem) return db[id] ? { status: 200, body: db[id] } : { status: 404, body: "not found" }; if (method === "POST" && isCollection) { const user = { id: nextId, name: "New" }; // server assigns the id db[nextId++] = user; return { status: 201, body: user }; // 201 Created } if (method === "PUT" && isItem) { db[id] = { id, name: "Updated" }; // replace wholesale (idempotent) return { status: 200, body: db[id] }; } if (method === "DELETE" && isItem) { delete db[id]; return { status: 204, body: null }; // 204 No Content } return { status: 405, body: "method not allowed" }; } // Drive it with the five CRUD requests: const script = [ ["GET", "/users"], ["POST", "/users"], ["GET", "/users/43"], ["PUT", "/users/42"], ["DELETE", "/users/42"], ["GET", "/users/42"], ]; for (const [m, p] of script) { const res = handle(m, p); console.log(`${m.padEnd(6)} ${p.padEnd(10)} -> ${res.status} ${JSON.stringify(res.body)}`); }

That is the whole shape of a REST framework, boiled down. Express, Rails, Django REST, ASP.NET — all of them are elaborate, production-grade versions of this handle(method, path) switch. The uniform interface is exactly what makes such a compact router possible: because meaning lives in the method and the URI, the dispatcher never needs a bespoke case per operation.

Statelessness, versioning, and a nod to HATEOAS

REST inherits HTTP's statelessness and makes a virtue of it: each request must carry everything the server needs (auth token, parameters, body). The server keeps no per-client session between calls. That is precisely what lets you run a hundred identical API servers behind a load balancer — any of them can serve any request — and it's why REST scales.

Because APIs are contracts other people's code depends on, you can't just change them. The usual answer is versioning/v1/users vs /v2/users, or a version header — so old clients keep hitting the old shape while new clients move on.

Fielding's most-ignored constraint is HATEOAS (Hypermedia As The Engine Of Application State): responses should include links telling the client what it can do next, so a client can navigate the API by following links rather than hard-coding every URL — the way you browse a website by clicking, never typing raw URLs.

GET /users/42 → 200 OK { "id": 42, "name": "Alice", "_links": { "self": { "href": "/users/42" }, "orders": { "href": "/users/42/orders" } } }

In practice full HATEOAS is rare; most "REST" APIs are what purists call REST-ish. That's fine — but knowing the real constraints tells you which corners you're cutting and what you give up.

The older style is RPCremote procedure call — where you expose functions: you POST to /getUser, /createUser, /deleteUser, each a named verb, usually all over POST. REST inverts this: expose resources (nouns) and use HTTP's fixed verbs on them. RPC-style APIs (gRPC, JSON-RPC, and most GraphQL usage) can be a better fit when your problem is genuinely "call this action" rather than "manipulate this thing" — a transfer(money) or sendEmail() doesn't map cleanly onto CRUD. Neither is universally right; REST shines for resource-shaped data, RPC for action-shaped operations.

The most common mistake is thinking any API that returns JSON is "RESTful". It isn't. Two tell-tale anti-patterns give the game away:

REST is a discipline about using HTTP's verbs, status codes and idempotency correctly — not merely a data format. JSON is just today's favourite representation.