REST — The Architectural Style

Resources, verbs, statelessness, cache, uniform interface, HATEOAS. Fielding's PhD thesis applied to your CRUD endpoints.

Building Block Foundational
8 min read
rest http architectural-style api

What it is#

REST (Representational State Transfer) is an architectural style for distributed hypermedia systems, defined by Roy Fielding in his 2000 PhD thesis. It is not a protocol, not a framework, not a specification — it is a set of six architectural constraints that, when applied together, produce systems with desirable properties (cacheability, scalability, evolvability, partial-failure resilience).

The six constraints, in Fielding’s order:

  1. Client–server — separate concerns; the client knows about UI, the server knows about storage.
  2. Stateless — every request carries everything the server needs to process it; no session state on the server between requests.
  3. Cacheable — responses are explicitly labeled as cacheable or not.
  4. Uniform interface — a single uniform way to identify and manipulate resources (this constraint expands into four sub-constraints: resource identification via URIs, manipulation through representations, self-descriptive messages, HATEOAS).
  5. Layered system — intermediaries (caches, gateways, load balancers) are invisible to the client.
  6. Code on demand (optional) — the server may ship executable code (JavaScript) to extend client behaviour.

REST does not say “use HTTP”. HTTP just happens to be the protocol everyone uses to build RESTful systems, because HTTP was designed by the same person, around the same time, with the same constraints in mind.

When to use it#

Reach for REST when:

  • The consumer is a browser or a polyglot ecosystem of clients. REST over HTTP is the most-supported API style in every language, framework, debugger, proxy, and gateway.
  • Resources are the right abstraction for the domain. If the verbs you’d want are mostly CRUD-shaped (POST /orders, GET /orders/123, PATCH /orders/123), REST fits.
  • Cacheability matters. REST’s cache constraint is load-bearing — CDN-friendly URLs, ETag headers, conditional GETs all fall out of doing REST well.
  • You want documentation tooling for free. OpenAPI / Swagger / Redoc generate readable docs straight from the schema.

Avoid REST (or be careful) when:

  • The interaction is fundamentally function-call-shaped (Translate(text, lang), ComputeETA(from, to)). RPC or GraphQL fits better.
  • The client needs to query arbitrary subsets of a resource graph. GraphQL’s whole reason to exist.
  • The traffic is bidirectional or streaming. WebSocket or gRPC streaming, not REST.
  • The fan-out across resources is too granular. A mobile screen that needs 12 REST calls before rendering is the canonical “we should have built GraphQL” moment.

How it works#

Resources, identifiers, representations#

In REST, everything addressable is a resource, every resource has a URI, and the server exchanges representations of resources (most commonly as JSON, but XML, HTML, or Protobuf are equally valid).

A REST request
GET /v1/orders/ord_a3f9c2 HTTP/1.1
Host: api.example.com
Accept: application/json
Authorization: Bearer eyJhbGciOi...
The response
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "W/c8f3"
Cache-Control: private, max-age=60
{ "id": "ord_a3f9c2", "status": "confirmed", "amount": { "value_minor": 4999, "currency": "USD" } }

The URL identifies the resource. The verb (GET) says what to do with it. The headers carry metadata. The body carries the representation. Every part is doing one job.

The verb taxonomy#

VerbSemanticsIdempotent?Safe?
GETRead a resourceyesyes
POSTCreate a resource (server picks ID)nono
PUTReplace a resource (client picks ID)yesno
PATCHPartial updatedependsno
DELETERemove a resourceyesno
HEADRead response metadata (no body)yesyes
OPTIONSDiscover allowed verbsyesyes

Safe means it does not change state on the server (a GET can have side-effects in practice — logging, view counters — but those don’t change resource state). Idempotent means N identical requests have the same effect as 1.

A representative client across three languages#

The same POST /v1/orders call in Python, Go, and Node. The wire format is identical; the language is style.

Create an order — Python
import requests
import uuid
resp = requests.post(
"https://api.example.com/v1/orders",
headers={
"Authorization": "Bearer eyJhbGciOi...",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
json={
"items": [{"sku": "SKU-X42", "qty": 1}],
"shipping_address_id": "addr_18df",
},
timeout=5,
)
resp.raise_for_status()
order = resp.json()
print(order["id"], order["status"])

Status codes that fit#

REST leans on HTTP’s status taxonomy. The five families:

  • 2xx success200 OK (read), 201 Created (write that produced a new resource), 202 Accepted (async), 204 No Content (success with no body).
  • 3xx redirect301 Moved Permanently, 304 Not Modified (cache hit).
  • 4xx client error400 Bad Request (malformed), 401 Unauthorized (missing auth), 403 Forbidden (wrong auth), 404 Not Found, 409 Conflict (state mismatch), 422 Unprocessable Entity (semantically wrong), 429 Too Many Requests (rate limit).
  • 5xx server error500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable, 504 Gateway Timeout.

A well-designed REST API picks the right code, every time, consistently. Returning 200 with { "error": "..." } is the most common rookie mistake.

Variants#

VariantMechanismWhen it fits
Strict REST (HATEOAS)Server returns hypermedia links in every response ({ "_links": { "next": "/orders/124" } }).Highly evolving APIs where clients should not hard-code URLs. Rare in practice; HAL and JSON:API are the formal forms.
REST-ish JSONResources + verbs + status codes, but no hypermedia. The de facto industry standard.Almost every REST API in production today.
REST + filtering DSLQuery parameters express filters / sorts / pagination (?status=confirmed&sort=-created_at&limit=20).Read-heavy APIs where the client needs flexibility without GraphQL’s complexity.
REST + bulk endpointsA POST /orders:batch collective verb when individual creates would N+1.High-throughput integration APIs (Stripe Invoices, BigQuery jobs).

Trade-offs#

What REST gives you:

  • The biggest tooling ecosystem on the planet. Every language, framework, proxy, and gateway knows REST.
  • Cacheability for free. A well-designed REST API gets CDN, browser, and gateway caching by following the constraints.
  • Browser-friendliness. REST works in every browser without a code-generated client.
  • Evolvability. Adding a new resource, a new endpoint, a new field — all additive. Old clients ignore what they don’t recognise.

What REST costs you:

  • Over-fetching and under-fetching. A mobile screen that needs a partial slice of three resources either pays N+1 round-trips or accepts whole-resource bloat. GraphQL exists for this reason.
  • Verbosity on internal services. Encoding a function call as a verb-on-resource is sometimes more ceremony than the operation deserves.
  • The HATEOAS debate every quarter. Strict REST without HATEOAS is technically not REST; few production systems follow it. You will have this conversation.
  • Per-call latency. Each request is a round-trip; HTTP/2 multiplexing helps but doesn’t eliminate the cost.

Common pitfalls#

  • Returning 200 with an error body. Use the right status code. Clients can’t distinguish “the call succeeded” from “the call failed with a payload” if both come back as 200.
  • Tunnelling RPC through POST /do_thing. If your “REST” endpoints are mostly verbs, you’ve built RPC over HTTP. That’s fine — call it RPC and pick the right tooling.
  • No versioning strategy. Then breaking changes ship inside /v1 and someone’s integration breaks on Tuesday.
  • Inconsistent error envelopes across endpoints. One endpoint returns { "error": "..." }, another returns { "errors": [...] }, a third returns { "code": "...", "message": "..." }. Pick one. Document it.
  • PUT vs PATCH confusion. PUT replaces the entire resource (idempotent). PATCH partially updates (often not idempotent unless you use JSON-Patch or include a version token).
  • No idempotency keys on POST. Every retryable write needs one. Stripe’s Idempotency-Key header is the industry-standard pattern.
Search ESC

Keyboard shortcuts

Shortcuts are disabled while typing in inputs.