The "REST Purity" Anti-Pattern
Contorting every operation into one interpretation of REST hides domain semantics behind status flips and produces contracts nobody can read. Clarity and domain meaning outrank purity — and so does the opposite ditch, where "REST is limiting" excuses a verb for everything.
Frame the contract
API design starts with a consumer, a design question and a guarantee — never with a URL.
Purity in one direction hides the domain
The purist reading says every operation must be a method on a noun. Under that rule "approve the expense report" becomes PATCH /expense-reports/42 {"status": "approved"}. The request looks clean and says nothing true: approval requires a role, records who approved, may need a second approver above a threshold, triggers a payment, and cannot be undone by patching the status back. All of that is now hidden in server code and discovered by consumers through trial (Resources Have State Machines).
Merging two customer accounts, retrying a failed job, transferring ownership, issuing a partial refund — none decompose into a field edit without lying. The honest shapes are a sub-resource that models the action with its data (POST /expense-reports/42/approvals), or a command sub-path when the operation is atomic and unreferenced afterwards (POST /jobs/42/retry). The reasoning lives in Resource or Action?; the point here is that refusing those shapes on aesthetic grounds is a design defect, not a virtue.
1PATCH /accounts/172{ "merged_into": 42, "status": "merged" }3 4# Which data moves? Which id survives? Is it reversible?5# What if 42 was itself merged yesterday?6# Retry after timeout: did the merge run twice? Nothing to ask.1POST /account-merges2Idempotency-Key: 91af…3{ "source": "acc_17", "target": "acc_42", "strategy": "keep_target_profile" }4→ 202 Accepted5{ "id": "mrg_3", "status": "running", "conflicts": [] }6 7GET /account-merges/mrg_3 # progress, conflicts, outcomeThe merge has data, takes time, can conflict and must be auditable. Giving it an address costs one resource and returns retryability, progress and history — the PATCH shape spent all three to satisfy a naming rule.
Purity in the other direction throws away the interface
"REST is too limiting" is the mirror-image failure, and it usually ends in POST /api/getUser, POST /api/updateUser, POST /api/deleteUser — an RPC vocabulary wearing HTTP as a transport (API Anti-Patterns Field Guide). Everything the uniform interface bought is gone: reads are not cacheable because they are POSTs, retries are unsafe because nothing is marked idempotent, monitoring sees one method, and every consumer learns a bespoke verb list instead of a known one (HTTP Methods Are Promises).
If the domain really is operation-shaped — commands with no meaningful nouns, high-volume internal calls — that is a reason to choose an RPC style openly, with its own tooling and contract discipline (RPC: Operation-Oriented Contracts, gRPC: Schema, Codegen and Streams), not to leak RPC through REST. The half-measure has the costs of both and the benefits of neither.
| Shape | Symptom | What was lost | Honest alternative |
|---|---|---|---|
| CRUD flattening (purity) | Every operation is a field edit; side effects undocumented | Domain semantics, retry story, audit, addressability | Action-as-resource or explicit transition sub-path |
| Verb explosion ("REST is limiting") | POST /getX, /doY, /deleteZ; one method for everything | Caching, safe retries, per-endpoint observability, known vocabulary | Real REST for resources, or an openly chosen RPC style |
| Pragmatic middle | Nouns for things, sub-resources for actions with data, command paths for atomic transitions | A little uniformity, deliberately | Recorded per-operation reasoning so the pattern is extendable |
A rule better than "no verbs"
Reviewers need a test that produces the middle road. Ask of each operation: does it carry data beyond a new state? Will anyone reference it later? Can it take time or fail halfway? Can it happen more than once? Yes to any of them means it deserves a resource. No to all of them and it is either a plain attribute edit (PATCH) or an atomic command (POST /…/verb). Then ask the interface questions: is it cacheable, is it safe to retry, does a proxy need to know? Those decide the method, and the method must tell the truth (GET: The Promise of Safety, POST: More Than Create).
The last requirement is consistency: whichever pattern the API picks for commands, it picks once and writes down (One Vocabulary: Naming and Consistency). An API with /orders/{id}/cancel, /invoices/{id}/cancellations and PATCH /shipments {status: "cancelled"} for the same kind of operation has not been pragmatic — it has been inconsistent, and inconsistency is the purist's best argument.
- Data, reference, duration, repetition → action-as-resource.
- Atomic, server-owned, never referenced → command sub-path with an honest method.
- Pure attribute change →
PATCH, and say so in the field docs. - Whole domain is operation-shaped → choose RPC openly, with its tooling.
- One convention per API, recorded where reviewers look.
Key points
- Purity that flattens domain operations into status edits hides semantics, side effects and retry behavior.
- The opposite purity — "REST is limiting" — leaks RPC through POST-everything and loses caching, safe retries and observability.
- The discriminating test is operational: data carried, addressability, duration, repetition decide the shape; cacheability and retry safety decide the method.
- If the domain is genuinely operation-shaped, choose an RPC style openly rather than smuggling it through REST.
- Consistency is what makes pragmatism defensible: one convention per API, written down.
Follow the failure
How the contract fails or gets misused, hop by hop — and what it costs when it completes.
- 1Reviewer → design: rejects
POST /orders/{id}/cancellationsas "a verb-ish thing"; the team shipsPATCH {status}instead. - 2Product → operation: cancellation grows a reason, an approver and a refund; each is bolted onto the order row as nullable columns.
- 3Client → API: retries a timed-out PATCH; the refund side effect fires twice; no cancellation record exists to reconcile.
- 4Second team → reaction: declares REST unworkable and builds
POST /api/cancelOrder,POST /api/getOrderfor the next service. - 5Gateway → metrics: half the company's reads are uncacheable POSTs; the other half hide domain operations in status fields.
- Consumers cannot predict side effects or retry safety from the contract; they learn by causing incidents.
- Audit and support have nothing to point at for operations that were flattened into field edits.
- RPC-through-REST loses HTTP caching, safe retries and per-endpoint observability while still claiming to be REST.
- Inconsistent command shapes across one API multiply the vocabulary every integrator must learn.
Design, observe, evolve
A contract decision is incomplete until you know how you would notice it failing and how it changes later.
- • Judge operations by data carried, addressability, duration and repetition — not by grammar.
- • Model qualifying actions as sub-resources; use command sub-paths for atomic server-owned transitions; reserve PATCH for attribute edits.
- • Keep method semantics truthful regardless of shape: reads on GET, unsafe operations never on GET, idempotency stated.
- • If most operations are commands with no nouns, choose RPC/gRPC explicitly and get its tooling.
- • Record the convention and apply it uniformly; make the reviewer test part of the design checklist.
- • Status fields accumulating sibling columns (`cancel_reason`, `cancelled_by`, `refund_state`) show flattened actions.
- • Duplicate side effects after retries reveal transitions with no addressable record.
- • A high share of POST traffic on read-only operations means the uniform interface was abandoned.
- • Multiple command shapes for the same kind of operation across an API surface in linter or review findings.
- • A flattened operation can be promoted to an action resource additively, keeping the field edit as a deprecated alias during migration ([[api-migration]]).
- • POST-everything endpoints can be given honest GET siblings for reads, then the POST variants deprecated with telemetry.
- • An API that discovers it is operation-shaped can move its internal consumers to gRPC behind a facade without touching public resources.
- • The reviewer test takes judgment per operation; a blanket rule is faster to apply and wrong more often.
- • Action resources add surface (ids, GETs, retention) that trivial toggles do not need — the test exists to avoid paying it everywhere.
- • Allowing command sub-paths invites drift unless the convention is enforced; consistency is the cost of pragmatism.