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 Commit
s 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 commit
s 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.