skip to content

Non-CRUD Actions

How to model operations that don't map to create/read/update/delete — cancelling an order, activating an account, running a search. Interviewers love this edge because it separates people who can only recite CRUD from people who can design state transitions RESTfully.

part ofAPI stylesoverview, primer and where to startread it →
on this pageshow

questions

3

Your HTTP API needs to let a client cancel an order. Cancelling is not a plain create, read, update, or delete. How do you model it, and what are the options?

level: middleimportance: must knowfreq 62%

answer

  1. action → noun: cancel/cancellation, refund, approval
  2. PATCH status = state machine option
  3. AIP custom method: POST /orders/{id}:cancel
  4. always POST, never GET for side effects
  5. 409 on already-cancelled

basics

~20 s

Three mainstream options: POST a state-transition sub-resource (POST /orders/{id}/cancellation), PATCH the status field, or an explicitly-marked custom method (POST /orders/{id}:cancel). Prefer the sub-resource when the action has its own data or history; keep POST for anything with side effects.

solid answer

~50 s

Not everything is CRUD, and forcing it produces worse APIs than admitting it. My options, in the order I consider them: 1. **Turn the action into a noun-shaped sub-resource**: `POST /orders/{id}/cancellation` with a body carrying reason and actor. This works best when the action has its own attributes, needs to be readable afterwards (`GET .../cancellation`), or can only happen once. 2. **Model it as a state change**: `PATCH /orders/{id}` with `{"status": "cancelled"}`, or a dedicated `PUT /orders/{id}/status`. Cheap and honest when cancellation is really just a field flip with no extra data. 3. **An explicit custom method**: `POST /orders/{id}:cancel`, the Google AIP convention — the colon marks it as deliberately outside the resource grammar rather than an accidental verb path. Whatever the shape, it is `POST` (unsafe, non-idempotent by default), returns a clear status — `200` with the updated order, or `409` if the order already shipped — and the outcome is observable in the order representation.

go deeper

for a junior

Know that the action goes on the order resource with POST, not GET, and that returning the updated order is expected.

for a middle

Compare the three shapes — sub-resource, status update, explicit custom method — and pick one with a reason; mention 409 for invalid transitions.

for a senior

Add operational concerns: retry semantics, idempotency, audit data on the action resource, and how the state machine is enforced server-side.

for a principal

Set the house rule for when actions may leave the CRUD grammar, and judge when an operation-heavy domain would be better served by an explicit RPC surface than by a strained resource model.

## The problem A resource-oriented HTTP API maps neatly onto data: collections, members, create/read/update/delete. Real systems also contain **operations** — cancel an order, retry a payment, publish a draft, rotate a key, send a password-reset email. These have preconditions, side effects beyond the record, and often their own audit data. Pretending they are field updates hides business rules; inventing `POST /cancelOrder` throws away resource orientation entirely. The craft is in choosing the least-bad shape per case. ## Option 1: the action becomes a resource Many actions have a natural noun: cancel → *cancellation*, ship → *shipment*, refund → *refund*, approve → *approval*. Creating that thing performs the action: `POST /orders/{id}/cancellation` with `{"reason": "customer_request"}` The benefits are concrete. The action's own data has a home (who, when, why). The result is addressable — `GET /orders/{id}/cancellation` returns the record, which is exactly what support tooling wants. Repeat submissions are naturally expressible: a second POST can return `409 Conflict` because a cancellation already exists. And if the operation grows attributes later, they extend a resource rather than bloating an update payload. The cost is invention: not every action has a comfortable noun, and `POST /users/{id}/passwordResetRequest` is a noun only in the sense that anything can be nominalised. ## Option 2: model the state machine If the action is genuinely "move this resource from state A to state B" with no extra payload, express that directly — `PATCH /orders/{id}` with `{"status": "cancelled"}`, or a sub-resource `PUT /orders/{id}/status`. The server still enforces the transition graph and rejects illegal moves with `409 Conflict` or `422`. This keeps the surface small and reads well when statuses are already part of the public representation. The weakness: it looks like a data write but runs business logic. A client that PATCHes several fields including status can be surprised at which side effects fire, and "cancelled" as a value gives you nowhere to put the reason without polluting the order body. ## Option 3: explicit custom methods Google's API Improvement Proposals define **custom methods** with a colon: `POST /v1/orders/{id}:cancel`, `POST /v1/users/{id}:activate`. The colon is the point — it visibly separates "this is a named operation on that resource" from the ordinary resource grammar, so a reader never mistakes `cancel` for a sub-resource collection. AIP guidance is deliberate: custom methods are a fallback used when standard methods do not fit, always POST unless the operation is a genuinely safe read (where GET is allowed), and named `verbNoun` in the underlying RPC. The practical caveats are ecosystem-level: colons in path segments are legal per RFC 3986 but occasionally trip naive routers, proxies, or client libraries, and the convention is much less familiar outside gRPC/Google-influenced APIs. ## What all three share - **POST, not GET.** These change state; GET must stay safe so caches and prefetchers cannot fire them. Do not accept a plain `GET /orders/{id}/cancel` for convenience. - **Preconditions produce real status codes.** Already-cancelled or already-shipped is `409 Conflict`; a semantically invalid request body is `422`; not permitted is `403`. - **The effect is observable.** Return the updated resource, or at minimum a link to it, so the client does not have to guess. - **Repeat calls are defined.** Decide explicitly whether a second cancel is a no-op `200`, a `409`, or deduplicated by a client-supplied key. ## The smell to avoid The failure mode is not "one custom action exists"; it is an API where *most* endpoints are POSTs to verb paths, resources are never addressable, everything returns `200` with a status field in the body, and HTTP is reduced to a transport for a homemade RPC. If you find yourself there, the honest fix is either to model the resources properly or to adopt an actual RPC framework — not to keep bolting verbs onto a URL space.

  • Why must a state-changing action never be exposed as a GET, even if it is convenient for testing in a browser?
    GET is defined as safe, so anything in the request path may issue it freely: caches revalidating, link prefetchers, crawlers, security scanners, and retry logic. A GET that cancels an order will eventually be triggered by something that had no intention of cancelling anything. Use POST and give testers a curl example instead.
  • How do you make a cancel action safe to retry after a network timeout?
    Either make it naturally idempotent — cancelling an already-cancelled order returns 200 with the same state rather than erroring — or accept a client-supplied idempotency key so the server recognises a repeat of the same logical request and replays the first outcome. Both remove the client's dilemma when a response is lost.

saying these in an interview costs you the question

  • Claiming REST forbids all non-CRUD operations so the action must be a PATCH
  • Exposing the action as GET /orders/{id}/cancel because it is easy to test
  • Returning 200 with an error message in the body when the order cannot be cancelled
  • Inventing a top-level /cancelOrder endpoint disconnected from the order resource
  • Leaving repeat-call behaviour undefined, so retries after a timeout are a gamble

context

open as a page

You inherit an HTTP API where almost every endpoint is a POST to a verb-shaped path returning 200 with a status field in the body. How do you judge whether that is a real problem, and what would you do about it?

level: principalimportance: should knowfreq 34%

basics

~20 s

Judge by cost, not purity: are resources unaddressable, are errors invisible to monitoring and gateways, are reads uncacheable, do retries misbehave? If yes, fix incrementally — model the core entities as addressable resources, use real status codes — or adopt an explicit RPC framework rather than a half-hearted hybrid.

open as a page

Google's API design guidelines allow custom methods written as POST /v1/users/{id}:activate, with a colon before the verb. What problem does that colon-suffix syntax solve, and what are its drawbacks?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

The colon visibly marks the last part as a named operation rather than a sub-resource, so readers and routers cannot confuse /users/1:activate with a collection /users/1/activate. Drawbacks: unfamiliar outside Google-style APIs, and colons occasionally trip naive routers, proxies, and tooling.

open as a page