# Actions That Are Not CRUD — Routebase

> Cancel, publish, retry, search and bulk edit, modelled three different ways, with what each modelling costs and the questions that decide between them.

Canonical page: https://routebase.dev/guides/api-design/actions-that-are-not-crud/
Chapter 4 of 10 · API Design · Last reviewed 2026-09-13 · The Routebase Team

Most of an API maps onto create, read, update and delete without complaint. Then somebody has to cancel an order, publish a document, retry a failed job or run a search with forty filters, and the mapping stops being obvious.

Forcing a noun onto every one of those produces worse names than the verb you were avoiding. This chapter is about picking deliberately between the options rather than defaulting into one.

## Three ways to model a state change

Take cancelling an order. All three of the following are used by well designed APIs, and they are not equivalent.

**An action sub-resource** puts the verb in the path, so a `POST` to the cancel path under the order performs it. This is the easiest to document, the easiest to authorise separately from a general update, and the easiest to give its own request body when a cancellation needs a reason. It is not idempotent, so a retry after a timeout hits the server twice.

**A state field** exposes the status as part of the representation and lets a partial update change it. This keeps the API uniform, since there is no new endpoint to learn, and it moves the rules into the server. It also invites a caller to attempt a transition that makes no sense, and rejecting one now needs a good error rather than a missing route.

**The action as its own resource** creates a cancellation that references the order. This is the most work and it gives you two things the others do not, which are a record of who cancelled and when, and a natural place for an asynchronous outcome. It suits actions that take time or need an audit trail.

## The questions that decide it

Four questions separate the three options faster than any principle does.

Does the action need input beyond the identifier? A cancellation reason, a publish target or a retry policy pushes towards an action endpoint or an action resource, because a state field has nowhere to put it.

Does the caller need the history? If somebody will ask which refunds were issued last month, the action wants to be a resource, since a state field remembers only the final state.

Is it slow? Anything that takes longer than a request should answer `202` and hand back a status resource, which is described in [methods and status codes](/guides/api-design/http-methods-and-status-codes/). That is an action resource in all but name, so you may as well make it one.

Can it be retried? None of the three is idempotent by construction, so a caller that times out has no safe move unless you provide one. That is the subject of [idempotency and retries](/guides/api-design/idempotency-and-retries/).

## Search when the query outgrows the URL

Search starts as a `GET` with query parameters, which is the right default. The request is safe, it is cacheable, and a caller can paste the URL into a ticket.

It stops being enough when the query gets large or sensitive. There is no fixed URL length in the HTTP specifications. [RFC 9112](https://www.rfc-editor.org/rfc/rfc9112) recommends only that a server be able to handle a request line of at least 8000 octets, and intermediaries are free to impose their own limits. A structured filter tree hits that ceiling sooner than it looks, and query strings end up in access logs whether or not the values belong there.

The usual escape is a `POST` that carries the query in the body, and it costs real things. You lose caching, you lose the shareable URL, and you have made a safe operation look unsafe to every intermediary. Sending a body on a `GET` is not the way around that, because RFC 9110 gives content on a `GET` no defined semantics and notes that a server may reject it.

The compromise that works is offering both, with the `GET` covering the queries that fit and the `POST` covering the rest. Two endpoints for one concept is a cost, and it is smaller than either of the alternatives.

## Bulk operations

A bulk endpoint exists because a caller has a thousand items and your rate limit has other ideas. The design decision is not the payload format, it is atomicity.

An atomic bulk operation either applies everything or nothing, which is simple to reason about and expensive to implement. A non-atomic one applies what it can, which means the response has to report per-item outcomes, since the caller needs to know exactly which entries to retry.

The failure to avoid is a `200` on a batch where half the items were rejected, with the failures buried in a body nobody parses. That reads as success to every generated client and to most retry logic, and the missing records surface much later. If the operation is not atomic, say so in the status code as well as the body, and give each item its own result.

## In Routebase

Actions are endpoints like any other, so the product surface for them is the naming and grouping that keeps them findable.

Every operation carries an operation identifier, generated from the method and path and overridable, and that identifier is what code generators turn into a method name in somebody's client library. Because an action endpoint is exactly where a generated name gets ugly, it is the place worth overriding. Uniqueness is enforced as an error rather than a warning, and a second rule keeps the identifier safe to use in a URL.

Grouping is the other half. Folders give the visible hierarchy that readers see in the published reference, while tags cut across it as flat labels. So a set of cancellation and refund endpoints can sit with their parent resources and still be collected under one tag. A tag description is exported as the OpenAPI tag description, which means the explanation of why these endpoints exist travels with the contract. The [Endpoints guide](https://docs.routebase.dev/endpoints/) covers folders, tags and operation identifiers.

## Frequently asked questions

### How do you model actions in a REST API?

There are three workable options. You can add an action sub-resource under the item, or you can expose the state as a field and let a partial update change it. The third option is to make the action itself a resource that gets created. Each is defensible, and they differ in whether the action can carry input, whether it can be retried safely and whether the history of it is queryable afterwards.

### Is POST /orders/123/cancel RESTful?

It is a widely used pattern and no specification forbids it, so the useful question is what it costs rather than whether it qualifies. An action sub-resource is easy to document and easy to authorise separately, and it is not idempotent by default, which means a retried cancel needs either a guard on the server or an idempotency key.

### Should search use GET or POST?

GET is the better default because the request is safe, cacheable and shareable as a URL. POST becomes necessary when the query no longer fits reliably in a URL, or when it carries values you should not put in a log. RFC 9112 only recommends that servers accept a request line of at least 8000 octets. The cost of moving to POST is that you lose caching and the ability to link to a result.

### How should a bulk API endpoint report partial failure?

Decide first whether the operation is atomic, because that decision drives everything else. An atomic bulk operation fails as a unit and can answer with a single error, while a non-atomic one has to return a per-item result so the caller knows which entries to retry. Answering 200 for a batch where half the items failed is the version that causes silent data loss downstream.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
