If you happen to develop API for non-trivial app with complex business logic beyond CRUD directly mapped to the database tables (i.e. typical Active Record pattern) you were probably wondering many times how to handle these cases and what's the best way to do it. There are quite a few solutions to this problem: you could add another endpoint for handling given use case, add non-RESTful action to already existing endpoint or you can add a magic param to the payload that would force non-standard scenario in the API. Let's take a closer look at the these solutions and discuss some advantages and disadvantages of each of them.
Our use case: creating drafts and articles
Let's start with some feature that will be simple enough for the blog post, but still interesting enough that we will have several ways to solve some the problem. Creating drafts and published articles (both referring to some
Article model) sounds good enough: for
published articles we are going to have different business validations and we will also need to have a possibility to transit from one state to another. Let's assume that the drafts don't have any required attributes at all and published articles require presence of:
author_id. Besides creating both type of articles, we will also need to update both type of articles while maintaining current state and have a possibility to transit from drafts to published articles. To add some extra logic articles will also have
published_at attribute, which is not directly settable via API, but will be automatically set server-side when making a transition from
published state or directly creating published article.
Let's start with the simplest strategy that I call
magic param or
virtual param. Basically, to force a different scenario we simply send some extra param, let's name it
published. In such case we will only have a single endpoint:
update actions and depending on the value of
published param we will either run a logic for creating / updating drafts or published articles.
Making transition from
published state is going to be the same, we simply need to send
published param with
true value and make sure other requirements are met (title's, content's and author's presence).
Obviously, this approach looks pretty simple from the client perspective, everything is driven by sending a magic param and this solution is pretty easy to integrate with some client-side data layers like Ember Data. But personally I don't find this solution really clean server-side. Having one interface (endpoint) for multiple purposes (where the logic is conditionally driven by a specific value of some param) adds some extra complexity on the design. Expecially in this case where the client knows exactly that we want to handle either drafts or published articles. For some features it may be good to hide some internal details server-side, but here it's not the case. So the solution to this problem might be...
Adding extra endpoint
With extra endpoint we clearly separate interfaces between both types of articles. Clearly, the logic on server is now much less complex. If we want to create a draft, we just use
Drafts endpoint. If we need to update a published article, we simply use (surprise, surprise)
As a side-effect of such design decision we can actually do some cool things, e.g. in
index actions we could return only one type of article, either drafts or published ones, depending on the endpoint.
The disadvantage of this approach is that it may not be that easy to handle it client-side with frameworks' data layers, which in most cases are kind of equivalents of Active Record pattern, but in API world.
Adding extra endpoint solved one of the issues: having the same interface for different kind of logic. But we still need to have a possibility to make a transition from
published article. One way would be to simply use
update action from Published Articles endpoint, but this would mean using endpoint for actually different resource, which looks really weird and not proper, especially after separating Articles to two different endpoints. We could still handle it with magic
published param, but this time only in Drafts endpoint. Or we could consider doing yet another thing, which is...
Adding extra action
This way doesn't sound really RESTful, but I find it pretty elegant. To handle transition from
published we can simply add
publish action to Drafts endpoint and let the API handle all the necessary logic.
What I like about combining different endpoints with extra actions is that everything is explicitly defined: every part of API has its' own interface and there's no magic and no implicit assumptions. It's also more flexible and removes some complexity server-side. The disadvantage of this approach is that it actually moves complexity to the client - having extra actions, again, may not be that easy to handle neatly by data layers (if you happen to use Ember and Ember Data, you can play with
buildURL function to make it smooth).
There are couple of different ways of handling complex logic in non typical CRUD scenarios in your API: using magic param, adding extra endpoints, adding extra method and each of them has its' own advantages and disadvatages on both server-side and client-side.