HATEOAS with Ember Data

Simon Taranto

Ember Data introduced strong conventions around how to structure API responses. While these conventions allow us to move quickly, there are additional steps we can take to minimize the coupling between the front end and back end. Using concepts from HATEOAS (Hypermedia as the Engine of Application State) we can make our Ember applications more flexible and resilient to changes on the server.

Ember Data

Ember Data can take advantage of API responses containing a links key pointing to URLs to associated resources. Ember Data will automatically pick up on those links as the source of the association’s data instead of using the default URL structure. In a use case where we want to asynchronously fetch associated resources, our API has more flexibility around URL structure.

Let’s say we have a Repo resource that has many Commits and we want the commits to be loaded asynchronously in Ember. How would we set up our API endpoints?

URL structure

By default, Ember Data is going to fetch the commits with a call that looks like /commits?ids[]=1&ids[]=4. For our use case, it makes more sense to have Ember ask for all of a repo‘s commits without having to know the commit id’s ahead of time. We’d like our endpoint to look like repos/1/commits. With this structure the client is able to make requests such as, “Please give me all the commits for this repo” instead of requests like “Please give me these specific commits”.

The JSON

Now that we have our URL structure, let’s implement the responses themselves. Using ActiveModel::Serializer, our Repo serializer looks like this:

class Api::V1::RepoSerializer < ActiveModel::Serializer
  attributes(
    :id,
    :links,
    :name,
  )

  def links
    { commits: "/repos/#{object.id}/commits" }
  end
end

which produces the following JSON at the /repos/1 endpoint:

{
  "id": 1,
  "links": {
    "commits": "/repos/1/commits"
  },
  "name": "hound"
}

The front end

We can instruct the Repo model to fetch the commits asynchronously:

import DS from "ember-data";

export default DS.Model.extend({
  name: DS.attr("string"),
  commits: DS.hasMany("commit", { async: true }),
});

We’re telling Ember Data to lazily fetch the commits from our specified endpoint. As a result, if we later change our minds that we want the commits resource to be available at a different URL, we can make that change without touching the front end. The server is able to change its URL structure with no impact on the front end.

The HATEOAS Dream

The idea of returning links is not a new one. HATEOAS has been around for some time and has a specific spec. It includes a more structured links object including rel and href keys and a complete URL containing the host name. Ember Data though asks you to follow the convention of naming the keys in links to match the resource name. A strict HATEOAS implementation of the above repo and commits example would look like:

{
  "id": 1,
  "name": "Hound",
  "links": [ {
    "rel": "commits",
    "href": "http://yourserver.foo/repos/1/commits"
  } ]
}

HATEOAS means flexibility

By combining stronger conventions around our JSON responses with smarter clients (Ember Data, in this case) we can build more flexible apps with little to no extra effort.

What’s next