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.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
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.
| Constraint | What it buys | What you lose when you drop it |
|---|---|---|
| Addressable resources | Linkability, bookmarking, cacheable GETs, references in events and docs | Everything is a POST body; nothing can be pointed at or cached |
| Uniform interface (methods) | Proxies, caches and retry libraries behave correctly by default | Every intermediary must special-case your API; retries become dangerous |
| Representations | Serialization is negotiable; the resource outlives the format | The wire format is the model; format changes are rewrites |
| Statelessness | Any instance answers any request; retries are safe repeats; scaling is horizontal | Sticky sessions, replay ambiguity, server-side state to migrate |
| Cacheability stated | CDNs and browsers absorb read traffic without code | Every 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.
POST /orders/ord_42/cancellations
Authorization: Bearer …
Idempotency-Key: 5c0d…
Content-Type: application/json
{ "reason": "customer_request", "restock": true }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.
- 1Team → schema: generates one
/tableresource per database table with GET/POST/PUT/DELETE and calls it REST. - 2Client → API: cancels an order by
PATCH {status: "cancelled"}; the refund side effect is undocumented and fires on a retry. - 3Gateway → cache: a
GET /users/mewithoutCache-Controlis cached by a CDN default and served to another user. - 4Server → session:
GET /ordersfilters by a server-side "current project" set in an earlier call; the retry lands on another instance and returns the wrong list. - 5Partners → docs: the OpenAPI file was hand-written a year ago; three fields have changed type since.
- 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.
- • 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.
- • 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.
- • 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.
- • 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.