Monday, January 14, 2013

Hypermedia API is about DRY

The hype around Hypermedia API (HATEOAS) just got too much recently. Opponents and advocates cross swords. But critics overlook one main point: Hypermedia API allows you to not repeat youself.

I could describe the topic in terms of "Hypermedia as the Engine of Application State". The "State Engine" directly relates to the issue. But I prefer to use more familiar dictionary.

Consider yet another project tracker. There is a domain rule: only Project Owner is able to delete a user story AND only stories in "initial" state can be deleted. With the "traditional" REST approach the rule gets duplicated several times: on the server side and for each client app. Without such duplication a client cannot reason about visibility of the "delete story" control. Even worse, the rule is implemented in several languages, increasing the likelihood of errors.
# Server side
def deletable_by?(user)
  user.owner? @project && initial?
end
// Android client
private boolean isDeletableBy(User user, Story story) {
  return user.isOwner(story.getProject()) && story.isInitial();
}
...
if (isDeletableByUser(user, story)) {
  renderDeleteWidgetFor(story);
} else ...

// JavaScript client
function isDeletableBy (user, story) {
  return user.isOwner(story.project) && story.isInInitialState()
}
...
<% if (isDeletableBy(user, story) { %>
  <%= renderDeleteWidgetFor(story) %>
<% } %>
With HATEOAS the rule is implemented once and only once. It doesn't spread domain logic across clients. They become fairly "silly".
# JSON generation on the server side
if story.deletable_by? current_user
  json_builder.delete_link_for story
end
// Android client
if (story.hasDeleteLink()) {
  renderDeleteWidgetFor(story);
} else ...
// Javascript client
<% if (story.hasLink('delete') { %>
  <%= renderDeleteWidgetFor(story) %>
<% } %>
See, clients don't "figure out" or "calculate" button visibility. They render the button only if the "delete" link is present. That's all. It simplifies the iterative development process: changes in domain logic require only server code to be rewritten.
# Changed rule
def deletable_by?(user)
  user.owner? @project
end
JavaScript, Android, iOS clients remain untouched. They still rely on absence/presence of the "delete" link. The "traditional" REST approach requires to rewrite/recompile/redestribute ALL client applications!

Summing up: Hypermedia API keeps domain logic on the server side. It reduces cost and encourages improvements.

2 comments:

  1. You have chosen very simple case. Existance/non-existance of link is a single bit of information.

    What if you need to pass more information?

    What if permission depends not only on server state but client's also? (Is user allowed to comment another user's post?)

    Domain logic will always leak. Resistance is futile.

    ReplyDelete
  2. >> You have chosen very simple case.
    It's surprising that really smart people don't see the advantage I've described in the post. It prompted me to write this post. And for sake of simplicity I deliberately simplify the example.

    >> What if permission depends not only on server state but client's also? (Is user allowed to comment another user's post?)
    Don't see any problem at all. My JSON builder on the server side will do something like this:

    if article.commentable_by? current_user
      builder.link :add_comment,
        url: article_comments_url(article),
        method: :post
    end

    And clients will show the "add comment" widget for a particular article only if JSON contains the "add_comment" link.

    ReplyDelete