HTTP approach proposes such methods to handle resources:
-
POST
for creation. -
GET
for retrieving information. -
PUT
for replacing a resource with the given content. -
PATCH
for partially updating a resource. -
DELETE
for deleting the resource.
There are other HTTP methods like HEAD or OPTIONS but mostly they are either generated by frameworks or used only due to technical concerns. So, we’re not discussing them in this article.
Mostly these methods are sufficient to build RESTful API. However, some cases do not align well with predefined methods.
In this article, I'm telling you:
- The problem with mapping business operation to a set of predefined HTTP methods.
- Ways to implement custom methods.
- Pros and cons of each solution.
The problem with mapping business operation to a set of predefined HTTP methods
Consider we're developing an email service. A user created a draft and now they want to send it. What HTTP method should you apply in this case? There are multiple options:
POST /drafts/{draftId}
Well, if you send a draft with POST
, then how do you create a new draft?
PATCH /drafts/{draftId}
{ "status": "sent" }
In this case, we don't introduce any specific endpoint. There is one that updates the draft
resource content. Therefore, we can call it to change the draft
status to sent
value. It supposes to trigger mail sending.
The advantage is that your API remains concise. But there are also crucial drawbacks:
- The flow might be misleading for a client. It’s not really obvious that such an operation should send a draft.
- The approach is error prone from a client's perspective. If a client sends
status
field by mistake, it might trigger draft sending. Even if a client just wanted to update the draft content. - The code on the backend also becomes more complicated. Developers have to add
if
cases for the endpoint that triggers draft sending if there is a particular field in the request. - Authorization for sending a draft can be different from a regular draft update. Again, more complicated scenario on the backend.
- Sending a draft can have different semantics. For example, if a user triggers draft sending, the server might return some
jobId
representing the status of the operation. But regularPATCH
invocations should occur synchronously. Meaning that return values also differ, which makes API more complicated.
The example assumes that there is a single resource
draft
. Other implementations can have different resources for drafts, received emails, and sent emails. But I skip these details for the sake of simplicity.
I’ve described a rather simple scenario. And even this one requires some sort of custom methods. Let’s discuss possible implementations.
Sub-resource
This is the most straight-forward one. I bet you’ve seen such examples often. Look at the code snippet below:
POST /drafts/{draftId}/send
It’s a separate endpoint. So, we don’t have to deal with PATCH
overlapping. The problem here is not about implementation, but REST API principles. A path describes a resource. And it should be a noun. But here the send
word is a verb that is a marker for an action. It raises bizarre questions:
- What is a
send
? Is it an entity for a given resource? - Can you invoke
GET /drafts/{draftId}/send
? - Can you transfer
send
entity to another draft?
You may think that I’m just making these claims out of nowhere. But imagine that a new person comes to your project. If your product is small and you don’t have many endpoints, then probably nothing will go wrong. But if your services exposes dozens of endpoints and many of them have custom methods in the API, then a new business analyst or architect can waste time figuring out the meaning of that sub-resource that actually has never existed.
As a matter of fact, I recommend you to avoid this solution.
A special actions
endpoints
If you have custom methods, why can't it be a dedicated resource? Look at the code example below:
POST /drafts/{draftId}/actions
{ action: "send" }
That looks more intriguing. The pros are:
- This endpoint fits well with REST API philosophy.
-
GET
can return the list of all available actions. You can even add authorization (some actions are not available for specific users). - In some cases, updating existing actions with
PATCH
orPUT
might be useful.
Still, there are some cons:
- All custom methods map to a single endpoint. Which adds problems to the backend (complex scenario, extra
if
statements, complicated authorization, etc.). - Some actions require additional parameters. If you document your REST API with OpenAPI (which you should do), then you would have to distinguish parameters for different action with oneOf/anyOf clause. Some client generators have problem with dealing these conditions correctly. Therefore, unnecessary technical concerns occur.
I can say that this approach is valid and might be effective. But usually it's an overengineering.
Custom method with a query parameter
Look at the example below:
POST /drafts/{draftId}?method=send
I'm not stopping here because this approach is almost identical to the one with PATCH
that we've seen before. Pros and cons are the same (additional if
statements on the backend, unexpected errors on the client side due to wrongly passed parameters etc.). So, let's move forward.
Custom method with a colon punctuation mark
This approach is similar to sub-resource one, but has different semantics. Look at the code snippet below:
POST /drafts/{draftId}:send
The idea is simple:
- You declare all custom methods as
POST
ones. - Then you add the verb describing the custom operation divided from the resource path by a colon.
I see many advantages of such technique:
- A custom operation maps as a dedicated endpoint. Meaning that a generated client will have a separate function for sending a draft.
- Because of the presence of a separate endpoint code on the backend becomes easier (authorization, returning values etc.).
- You don’t have to worry about overlapping with existing regular resources.
- A colon punctuation mark clearly puts a difference between a resource and an action.
I’ve taken this approach from the book API Design Patterns. I strongly recommend you to read it if you deal with creating REST API in your daily work. There are lots of useful patterns besides custom operation methods.
Conclusion
Custom methods are necessary for the majority of applications. But you should apply the tool with caution. Don’t fall into the trap of creating too much custom methods. That makes your RESTful API actually RPC. However, if you need one, you know how to design it.
Thank you very much for reading the article! I hope you’ve learnt something new and it will help you to complete the work more efficiently. I’d be grateful, if you pressed a like button, left a comment and shared the piece with your friends and colleagues. Have a nice and productive day!
Top comments (6)
I think that the approach to this problem is too draft-bias. Your main resource is emails.
if you've had the pleasure of implementing or consuming API in the 2000s and early 2010s you'd have seen the 'variant route' method. Practically identical to the colon method just with a semicolon before google recommend a colon. But note it's controversial whether this is restful or not.
POST /drafts
<- new draftPOST /emails
<- new emailPUT /emails;draft
<- new email from draftbut really this whole domain doesn't need this complexity, you can store emails in a draft mode since the two models are exactly equivalent, hard-coupled, and can't diverge. But let's assume that they can or that you want to distinguish between them absolutely, you can still trivially express that
POST /drafts
<- new draftPOST /emails
<- new emailand then to send an email just
GET
the draft andPOST
to emails, done. Totally restful and no fancy shenanigans, also avoids side-effects. Doing two requests is irrelevant when you take http caching into accountyou can represent them as the same database table if you want, your API expresses your business not your implementation
They probably changed to
:
because;
is in the same class of reserved character as '&'.I would invert your layout by treating an action as a stage.
That's the way I'm usually implementing APIs. But I didn't know the colon in the context of API URLs, looks also very intuitive.
300%
This is the correct answer. Any verb can be made into a noun if you think harder. Result of an action is can be expressed by a noun. So your POST endpoint for the action should be named after the result you get. You don't have to persist the result if you don't want to.