How To Handle Non-CRUD Logic In Your API
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 drafts
and 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: title
, content
and 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 draft
to published
state or directly creating published article.
Magic param
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: Articles
with create
and 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 draft
to 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) Articles
endpoint.
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 draft
to 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 draft
to 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 adapters
and buildURL
function to make it smooth).
Wrapping up
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.