Stylesrestresourceshttprepresentationsstatelesscaching

REST as a Practical Style

REST is a set of constraints — addressable resources, uniform methods, representations, statelessness, cacheability — that let the whole HTTP ecosystem work for you unmodified. It is not "CRUD over HTTP", and its value is in the guarantees, not the URL aesthetics.

Follow the failure

Frame the contract

API design starts with a consumer, a design question and a guarantee — never with a URL.

Design question
What does designing "RESTfully" actually buy a consumer — and which of the constraints are worth keeping when the domain pushes back?
Consumers
Third-party developers who will integrate with curl and the docs, browsers and mobile apps behind CDNs and gateways, and any client that benefits from the enormous installed base of HTTP tooling — caches, proxies, SDK generators, monitoring.
The promise
Resources are addressable and stable, methods mean what HTTP says they mean, representations are self-describing, each request stands alone, and cacheability is stated — so every intermediary and client library behaves correctly without special knowledge of your API.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

Constraints, not conventions

REST is usually taught as a naming convention — plural nouns, no verbs, one path per table. That is the residue, not the idea. REST is a small set of constraints, and each one exists to buy a concrete property. Addressable resources with stable identifiers mean anything a consumer cares about can be linked, bookmarked, cached and referenced in a webhook (From Domain to Resources). A uniform interface — the same handful of methods with fixed semantics on every resource — means a proxy, a cache or a retry library can act correctly without reading your docs (HTTP Methods Are Promises). Representations decouple what a resource *is* from how it is *serialized*, so JSON today and something else tomorrow is a header negotiation, not a rewrite.

Statelessness means each request carries everything needed to process it; no server-side session decides what GET /orders returns. This is what makes horizontal scaling and retries trivial — any instance can answer any request, and a retried request is the same request. Cacheability means responses say whether and how long they may be reused, and the entire HTTP caching infrastructure — browser, CDN, gateway — honors it for free (Caching as a Contract Clause).

When you drop a constraint you lose its property, and that is sometimes the right trade. A batch endpoint gives up some uniformity for fewer round trips (Batch APIs and Partial Failure); a command sub-path gives up noun purity for domain clarity (Resource or Action?). The discipline is knowing which property you just spent, not refusing to spend any.

Each constraint buys something specific
ConstraintWhat it buysWhat you lose when you drop it
Addressable resourcesLinkability, bookmarking, cacheable GETs, references in events and docsEverything is a POST body; nothing can be pointed at or cached
Uniform interface (methods)Proxies, caches and retry libraries behave correctly by defaultEvery intermediary must special-case your API; retries become dangerous
RepresentationsSerialization is negotiable; the resource outlives the formatThe wire format is the model; format changes are rewrites
StatelessnessAny instance answers any request; retries are safe repeats; scaling is horizontalSticky sessions, replay ambiguity, server-side state to migrate
Cacheability statedCDNs and browsers absorb read traffic without codeEvery read hits origin; or worse, stale data is cached by accident

REST is not CRUD

The "REST equals CRUD" reduction is where most REST APIs go wrong. CRUD is a storage vocabulary; consumers do not want to update rows, they want to cancel orders, approve requests and retry jobs (The "Everything Is CRUD" Trap). REST handles those fine — a cancellation is a resource you create, an approval is a state transition with a documented contract (Designing State Transitions) — but only if the designer starts from the domain rather than from the tables.

The tell is an API where every resource has exactly the four CRUD operations and the response is the table row (Response Contracts Are Not Database Rows). Such an API is RESTful in shape and useless in practice: clients reconstruct business operations from field edits, side effects are undocumented, and state machines are enforced nowhere. The opposite mistake — every operation is POST /doSomething because "REST is too limiting" — throws away the uniform interface without gaining clarity. Both are covered in The "REST Purity" Anti-Pattern.

A domain operation expressed RESTfully — not as a row update
Request
POST /orders/ord_42/cancellations
Authorization: Bearer …
Idempotency-Key: 5c0d…
Content-Type: application/json

{ "reason": "customer_request", "restock": true }
Response
HTTP/1.1 201 Created
Location: /orders/ord_42/cancellations/can_9
Content-Type: application/json
Cache-Control: no-store

{
  "id": "can_9",
  "order": "/orders/ord_42",
  "status": "refund_pending",
  "initiated_by": "usr_7",
  "created_at": "2026-08-25T10:14:00Z"
}

What REST costs, honestly

Fixed response shapes are the structural cost: the server decides what GET /users/42 returns, so a client that needs three fields downloads eighty, and a client that needs five resources makes five calls (Over-Fetching and Under-Fetching). Sparse fieldsets and embedded expansions patch this partially; a Backend for Frontend patches it for one client; GraphQL solves it and charges for the solution elsewhere. JSON over HTTP/1.1 is also not the cheapest wire format — for high-volume internal traffic, gRPC: Schema, Codegen and Streams's binary framing and multiplexing are measurably lighter.

The type contract lives in documentation unless you add machinery: OpenAPI, validation middleware, contract tests (OpenAPI: Describing the Contract, Not Designing It, Testing the Contract, Not Just the Code). Without them REST's flexibility becomes drift — the docs say one thing, the server another, and clients discover the difference in production. REST's great advantage is that all of that machinery already exists and is boring; the cost is that you have to switch it on.

  • Server-decided shapes → over/under-fetching; mitigate with fieldsets, expansions, a BFF.
  • Text JSON on HTTP → serialization cost at internal scale; gRPC where it is measured to matter.
  • Types by documentation → drift unless OpenAPI + validation + contract tests are wired in.
  • Uniform interface → domain operations need modeling thought, not a verb per action.

Key points

  • REST is five constraints that each buy a property: addressability, uniform methods, representations, statelessness, stated cacheability.
  • The value is that the whole HTTP ecosystem — caches, proxies, retries, SDK generators — works without special knowledge of your API.
  • REST is not CRUD: domain operations are modeled as resources and transitions, not as row updates.
  • Dropping a constraint is a legitimate trade as long as you can name the property you spent.
  • REST's costs are fixed response shapes, text serialization and documentation-borne types — each has a known mitigation.

Progressive depth

Overview

REST lets every existing piece of HTTP infrastructure — caches, proxies, browsers, SDK generators — work with your API unmodified, because resources are addressable, methods have fixed meanings and responses say whether they can be reused.

Practical

Derive resources from consumer tasks, use GET for reads and honor its safety, POST for creation and commands, PUT/PATCH with defined semantics, and put Cache-Control on every response. Model domain actions as resources (/orders/{id}/cancellations) instead of status edits.

Advanced

Statelessness is what makes retries and horizontal scaling safe: any request can be replayed on any instance. Combine it with idempotency keys for POST, conditional requests for writes, and stated cacheability with validators so intermediaries can revalidate cheaply.

Internals

Underneath, HTTP/1.1 keep-alive and HTTP/2 multiplexing decide how many round trips a screen actually costs; CDN caches key on method, URL and Vary; and the JSON serialization path is often the largest CPU consumer on a hot endpoint — which is where gRPC's binary framing earns its place internally.

Follow the failure

How the contract fails or gets misused, hop by hop — and what it costs when it completes.

  1. 1
    Team → schema: generates one /table resource per database table with GET/POST/PUT/DELETE and calls it REST.
  2. 2
    Client → API: cancels an order by PATCH {status: "cancelled"}; the refund side effect is undocumented and fires on a retry.
  3. 3
    Gateway → cache: a GET /users/me without Cache-Control is cached by a CDN default and served to another user.
  4. 4
    Server → session: GET /orders filters by a server-side "current project" set in an earlier call; the retry lands on another instance and returns the wrong list.
  5. 5
    Partners → docs: the OpenAPI file was hand-written a year ago; three fields have changed type since.
What breaks
  • Intermediaries misbehave: caches store what they should not, retry libraries repeat what they should not, because method and cache semantics were not honored.
  • Clients reconstruct business logic from row edits and get it wrong; state machines exist only in the client that guessed best.
  • Horizontal scaling and failover break on hidden server-side state.
  • Documentation drift turns REST's strength — integrate from the docs — into its weakness.

Design, observe, evolve

A contract decision is incomplete until you know how you would notice it failing and how it changes later.

Design the contract
  • • Derive resources from consumer tasks and domain concepts, then map operations to methods honoring their semantics — never from tables outward.
  • • State cacheability on every response, including `no-store` where the answer is "never".
  • • Keep every request self-contained; anything that looks like session state belongs in the URL, a header or a resource.
  • • Wire the type machinery on day one: OpenAPI description, request validation, contract tests.
  • • Model domain actions as resources or explicit transitions, and document side effects where they happen.
Observe in production
  • • Cache hit ratio near zero on read-heavy endpoints, or cache-related data leaks, indicate cacheability was never stated.
  • • Clients making N calls to render one screen, visible in gateway logs as bursts per session, indicate fixed shapes hurting a specific consumer.
  • • Retry storms producing duplicate side effects reveal non-idempotent methods used where idempotent ones were promised.
  • • OpenAPI validation failures in CI, or their absence entirely, tell you whether the described contract is the real one.
Evolve without breaking
  • • Additive changes — new fields, new resources, new optional parameters — are the everyday evolution path and need no version ([[backward-compatibility]]).
  • • Sparse fieldsets and expansions can be added later to relieve over-fetching without changing existing responses.
  • • A REST surface can front a gRPC core via transcoding when internal traffic outgrows JSON.
  • • Content negotiation lets new representations appear beside JSON without touching resource identity.
What it costs
  • • Honoring method semantics constrains design: some operations need modeling thought that `POST /doIt` would skip.
  • • Stated cacheability requires deciding freshness per resource, a decision many teams would rather not make explicitly.
  • • The mitigations for fixed shapes (fieldsets, expansions) add query surface and validation work.

Misconceptions

Claim
“REST means mapping CRUD to GET/POST/PUT/DELETE.”
Reality
REST constrains the interface, not the domain. Cancellations, approvals and retries are resources and transitions; CRUD is a storage vocabulary that happens to fit some of them.
Claim
“Plural nouns and no verbs make an API RESTful.”
Reality
Naming is cosmetic. Honoring method semantics, stating cacheability and keeping requests self-contained are what let caches, proxies and retry libraries work — an API can have perfect nouns and violate all three.
Claim
“REST is slow compared to gRPC.”
Reality
Text JSON on HTTP/1.1 costs more per call than binary frames on HTTP/2, which matters on high-volume internal paths. For a public API the dominant costs are round trips and payload design, where REST with good caching often wins outright.

Apply it