Stylesanti-patternrestpragmatismdomain semanticsverbs

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.

Follow the failure

Frame the contract

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

Design question
When the domain pushes back against the resource model, do you bend the domain or bend the style — and how do you tell a principled exception from a lazy one?
Consumers
Integrators trying to perform a business operation — approve, cancel, merge, retry — who need to know what it does and whether it happened, and reviewers who need a rule better than "no verbs" to judge a design.
The promise
Operations are shaped by their semantics — data carried, addressability, repeatability, side effects — so consumers can read the contract and predict behavior; the uniform interface is kept wherever it buys something and consciously spent where it does not.
RequirementConsumersResource ModelStyleContractValidationAuthorizationErrorsIdempotencyPaginationVersioningObservabilityEvolutionTrade-offs

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.

Purity: the domain smuggled through a status field
1PATCH /accounts/17
2{ "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.
Semantics first: the operation as a domain concept
1POST /account-merges
2Idempotency-Key: 91af
3{ "source": "acc_17", "target": "acc_42", "strategy": "keep_target_profile" }
4202 Accepted
5{ "id": "mrg_3", "status": "running", "conflicts": [] }
6
7GET /account-merges/mrg_3 # progress, conflicts, outcome

The 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.

The two ditches and the road between them
ShapeSymptomWhat was lostHonest alternative
CRUD flattening (purity)Every operation is a field edit; side effects undocumentedDomain semantics, retry story, audit, addressabilityAction-as-resource or explicit transition sub-path
Verb explosion ("REST is limiting")POST /getX, /doY, /deleteZ; one method for everythingCaching, safe retries, per-endpoint observability, known vocabularyReal REST for resources, or an openly chosen RPC style
Pragmatic middleNouns for things, sub-resources for actions with data, command paths for atomic transitionsA little uniformity, deliberatelyRecorded 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 changePATCH, 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.

  1. 1
    Reviewer → design: rejects POST /orders/{id}/cancellations as "a verb-ish thing"; the team ships PATCH {status} instead.
  2. 2
    Product → operation: cancellation grows a reason, an approver and a refund; each is bolted onto the order row as nullable columns.
  3. 3
    Client → API: retries a timed-out PATCH; the refund side effect fires twice; no cancellation record exists to reconcile.
  4. 4
    Second team → reaction: declares REST unworkable and builds POST /api/cancelOrder, POST /api/getOrder for the next service.
  5. 5
    Gateway → metrics: half the company's reads are uncacheable POSTs; the other half hide domain operations in status fields.
What breaks
  • 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.

Design the contract
  • • 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.
Observe in production
  • • 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.
Evolve without breaking
  • • 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.
What it costs
  • • 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.

Misconceptions

Claim
“Verbs in paths are always wrong.”
Reality
A command sub-path for an atomic, server-owned, unreferenced operation is an honest contract. What is wrong is choosing a shape by grammar rather than by the operation's data, retry and audit needs.
Claim
“Pragmatic means anything goes.”
Reality
Pragmatic means the shape is chosen by a repeatable test and applied consistently. An API with three different command conventions is not pragmatic, it is unreviewed.
Claim
“If REST feels limiting, wrap RPC in POST and move on.”
Reality
That keeps REST's costs and loses its benefits. If the domain is operation-shaped, an explicit RPC style with schema and codegen is cheaper to own than RPC disguised as REST.

Apply it