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?
answer
- action → noun: cancel/cancellation, refund, approval
- PATCH status = state machine option
- AIP custom method: POST /orders/{id}:cancel
- always POST, never GET for side effects
- 409 on already-cancelled
basics
~20 sThree 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 sNot 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
Know that the action goes on the order resource with POST, not GET, and that returning the updated order is expected.
Compare the three shapes — sub-resource, status update, explicit custom method — and pick one with a reason; mention 409 for invalid transitions.
Add operational concerns: retry semantics, idempotency, audit data on the action resource, and how the state machine is enforced server-side.
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