DEV Community

Semyon Kirekov
Semyon Kirekov

Posted on

Custom methods in REST API endpoints

HTTP approach proposes such methods to handle resources:

  1. POST for creation.
  2. GET for retrieving information.
  3. PUT for replacing a resource with the given content.
  4. PATCH for partially updating a resource.
  5. 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:

  1. The problem with mapping business operation to a set of predefined HTTP methods.
  2. Ways to implement custom methods.
  3. Pros and cons of each solution.

The article meme cover

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}
Enter fullscreen mode Exit fullscreen mode

Well, if you send a draft with POST, then how do you create a new draft?


PATCH /drafts/{draftId}

{ "status": "sent" }
Enter fullscreen mode Exit fullscreen mode

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:

  1. The flow might be misleading for a client. It’s not really obvious that such an operation should send a draft.
  2. 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.
  3. 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.
  4. Authorization for sending a draft can be different from a regular draft update. Again, more complicated scenario on the backend.
  5. 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 regular PATCH 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. What is a send? Is it an entity for a given resource?
  2. Can you invoke GET /drafts/{draftId}/send?
  3. 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" }
Enter fullscreen mode Exit fullscreen mode

That looks more intriguing. The pros are:

  1. This endpoint fits well with REST API philosophy.
  2. GET can return the list of all available actions. You can even add authorization (some actions are not available for specific users).
  3. In some cases, updating existing actions with PATCH or PUT might be useful.

Still, there are some cons:

  1. All custom methods map to a single endpoint. Which adds problems to the backend (complex scenario, extra if statements, complicated authorization, etc.).
  2. 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The idea is simple:

  1. You declare all custom methods as POST ones.
  2. Then you add the verb describing the custom operation divided from the resource path by a colon.

I see many advantages of such technique:

  1. A custom operation maps as a dedicated endpoint. Meaning that a generated client will have a separate function for sending a draft.
  2. Because of the presence of a separate endpoint code on the backend becomes easier (authorization, returning values etc.).
  3. You don’t have to worry about overlapping with existing regular resources.
  4. 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!

Resources

  1. HTTP methods
  2. RESTful API
  3. OpenAPI
  4. OneOf/AnyOf/AllOf/Not clauses
  5. API Design Patterns book

Top comments (6)

Collapse
 
datner profile image
Datner

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 draft
POST /emails <- new email
PUT /emails;draft <- new email from draft

but 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 draft
POST /emails <- new email

and then to send an email just GET the draft and POST to emails, done. Totally restful and no fancy shenanigans, also avoids side-effects. Doing two requests is irrelevant when you take http caching into account

you can represent them as the same database table if you want, your API expresses your business not your implementation

Collapse
 
rouilj profile image
John P. Rouillard

They probably changed to : because ; is in the same class of reserved character as '&'.

Collapse
 
rouilj profile image
John P. Rouillard

I would invert your layout by treating an action as a stage.

method endpoint description
get /draft list child endpoints (save, send ...)
get draft/send list all sent drafts
get draft/send/{draftid} get delivery info (messageid, delivery status) for a draft
post draft/send/{draftid} send the draft
delete draft/send/{draftid} remove the info about the sent draft
get draft/save get all saved drafts
post draft/save save a new draft returns draftid
get draft/save/{draftid} get a saved draft's contents
post/put draft/save/{draftid} update draft content of whole draft message
patch draft/save/{draftid}/subject update just subject of draft. Similar for /to, /cc, /bcc, /body ...
delete draft/save/{draftid} delete saved draft
Collapse
 
mrdoe profile image
MrDoe

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.

Collapse
 
aleksey profile image
Aleksey Stukalov

300%

Collapse
 
dnshio profile image
Dinesh Vitharanage

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.