Modeling Actions as Resources
One idea I’ve internalized while designing APIs is that actions don’t always have to look like actions. Many actions are better off being actual resources.
It feels intuitive and logical to define endpoints like:
POST /subscriptions/:id/cancel
POST /invoices/:id/pay
It’s straightforward to build, easy for clients to use, and by itself it’s not inherently wrong. But again and again, I’ve noticed its limitations.
When an action becomes a resource
When you cancel a subscription, there’s a reason. Easy: just add a reason field to the POST body. But a user can also change their mind and withdraw the cancellation, keep the subscription for a few months, and then cancel again. When you store status: "canceled" and reason: "too expensive" in your subscriptions database table, and the user withdraws the cancellation… what do you do with this information? Do you just throw it away? It’s valuable information!
It’s also useful to be able to list all cancellation attempts of a user. It’s good information for someone like a customer support agent to see the user’s history during a support inquiry. “Listing cancellations” sounds a lot like a resource.
It’s similar for paying an invoice: a payment can fail, it’s more like a payment attempt. Also, storing this information in the parent resource is kinda… ugly. It turns into situations like these:
{
"type": "invoice",
"status": "payment_failed",
"failure_reason": "insufficient funds",
"payment_method": { /* ... */ }
}
// or
{
"type": "invoice",
"status": "paid",
"failure_reason": null,
"payment_method": { /* ... */ }
}
It often results in fields that are nullable (like failure_reason in the example above), but are never null when the status is "payment_failed". It’s a weird API contract: it’s not self-explanatory and you have to explain the behavior to an API consumer. The API is mashing two different concepts into a single resource.
So instead of modeling these actions as verbs attached to a resource, I now almost always reach for modeling the action as a proper resource:
POST /subscription_cancellations
POST /payment_attempts
All action-related properties are bundled inside one resource, no weird nullables. And most importantly, you don’t lose the history in your database. You can just list them all:
GET /subscription_cancellations?subscription=sub_123
GET /subscription_cancellations?user=usr_xyz
GET /payment_attempts?invoice=inv_000
I also usually model them as top-level resources, and not nested resources like /subscriptions/:id/cancellations. It might look handy at first sight, but having it as a top-level resource allows you to list all cancellations, or add more complex filters as URL parameters that aren’t possible as a subresource.
When it shouldn’t
A few times in this post, I’ve emphasized that this pattern is helpful almost always.
Last week I added a friend request feature to an API, so two users can connect with each other and become friends:
send a user a friend request:
POST /friend_requests { sender, recipient }
to respond to the friend request:
POST /friend_requests/:id/accept
POST /friend_requests/:id/ignore
After designing everything in the API as resources, it kinda hurt to define these action endpoints. But what’s the alternative?
POST /friend_request_responses?
Accepting or ignoring a friend request isn’t a useful independent identity by itself. You usually can’t decline a friend request, and then a bit later accept it again. The user would instead send a new friend request, and not update an existing one.
In my mind, I ask myself “do I want to refer to this action later?” If so, I model it as a resource.
It could also be:
PATCH /friend_requests/:id
{"response": "accept"}
However, I don’t like an API surface where you change a field and it kicks off some side effects. Accepting a friend request will create a new friendship resource between both users, it will send out a notification… that’s more than simply updating some data.
In comparison, when a user updates their profile:
PATCH /user_profiles/me
{"display_name": "Robinson Crusoe"}
…that just changes the name that’s shown in the app. It doesn’t kick off a chain of user-facing side-effects.
Resources as a debugging tool
As I mentioned above, having resources for these actions means that we can view their history. That’s not only useful for support folks, but also for us developers.
You can use it to trace events in your system, see how things progressed, and debug without painfully going through logs or by reading the code and guessing how a bug might have happened. You can store outcomes of a long-running background action, error messages, retry attempts… the stuff you would usually just log so you can debug errors when they might happen.
And you can turn complex processes, where one action fans out into multiple resources, into straightforward logic. For example:
- A user publishes a post.
- The system creates a Notification resource for each friend (or subscriber).
- For each Notification, a Push Notification is created. A user can have multiple devices, so each device needs its own Push Notification.
- When a Push Notification is created, it’s sent to the device. The outcome and potential error message from a push notification service is stored on the Push Notification resource.
Now, when a user complains that they didn’t receive a notification, you can go into the database and see what’s up: was a Notification created? How many devices are registered for the user? Was a Push Notification created for each device? Was there an error?
And it doesn’t stop there:
- you can now send any arbitrary Notification to all user’s devices, simply by creating a Notification resource.
- you can send a Notification to a single device, by creating the Push Notification resource directly. Create a silent data-only Push Notification to check if the device still has the app installed.
- you now have a Notification resource, which makes it easy to add a
GET /notificationsendpoint and add a Notification Center into the app.
Is this premature optimization?
Are actions as resources premature optimization? Saying “it might be helpful in the future” sounds a lot like YAGNI1. So, should you first build the action endpoint, and later turn it into a resource?
I don’t think so. It isn’t speculative future-proofing when the action already has domain data that I care about: The cancellation reason already exists. The payment failure already exists. Putting it into a resource decides where this information belongs, not whether it’s needed.
These days, my rule of thumb is simple: does the action have information on its own? Does it have a lifecycle? Is it something I want to inspect later? Then it’s a resource. If it’s just a state transition, I’ve learned not to force it.
It’s a small shift in how I model APIs, and it has consistently made the systems behind them easier to understand, extend, and debug.
Anyway, time for a walk.
Footnotes for Section Heading
Comment: [email protected]

