Handling Nested Resources and Relationship Links in Ember Data

Sunday, February 21st, 2016

Updated 10/24/2016

Many APIs use nested resource paths. That is, URL paths that contain a hierarchy of resource types. For example, a nested resource path might look something like: /users/5/pets, where there is a collection of pet resources under a user resource. How do you handle that in Ember Data?

Let’s say we have a user model with asynchronous belongsTo and hasMany relationships:

// app/models/user.js
export default DS.Model.extend({
  first: DS.attr('string'),
  last: DS.attr('string'),
  pets: DS.hasMany('pet', { async: true }),
  company: DS.belongsTo('company', { async: true })
});

Ember Data supports relationship links. What that means is that we can have a property called links on individual resource objects, which is an object that contains URLs that point to related data.

With the DS.RESTSerializer format, relationship links for a user resource object would look like:

{
  "users": {
    "id": 1,
    "first": "David",
    "last": "Tang",
    "links": {
      "company": "/api/v1/users/1/company",
      "pets": "/api/v1/users/1/pets"
    }
  }
}

With the DS.JSONSerializer format, relationship links for a user resource object would look like:

{
  "id": 1,
  "first": "David",
  "last": "Tang",
  "links": {
    "company": "/api/v1/users/1/company",
    "pets": "/api/v1/users/1/pets"
  }
}

With JSON:API, relationship links for a user resource object would look like:

{
  "data": {
    "id": "1",
    "type": "users",
    "attributes": {
      "first": "David",
      "last": "Tang"
    },
    "relationships": {
      "company": {
        "links": {
          "related": "/api/v1/users/1/company"
        }
      },
      "pets": {
        "links": {
          "related": "/api/v1/users/1/pets"
        }
      }
    }
  }
}

Not sure about the differences between the different serializer formats? Check out the post Which Ember Data Serializer Should I Use?

If we made a request to /api/v1/users, each user resource object in the response can have a links property. When you access user.pets or user.company, Ember Data will trigger a fetch using these URLs defined in links.

As noted in the API documentation:

The format of your links value will influence the final request URL via the urlPrefix method: Links beginning with //, http://, https://, will be used as is, with no further manipulation. Links beginning with a single / will have the current adapter’s host value prepended to it. Links with no beginning / will have a parentURL prepended to it, via the current adapter’s buildURL.

What if your API doesn’t return a links property, and this is how your related data needs to be accessed? I have found this to be a pretty common scenario.

To handle this, we can add these relationship links manually in our serializer during the normalization process. Specifically, we can override one of the normalization methods in the serializer like normalize(), normalizeResponse(), normalizeFindAllResponse(), etc, and create a links property for each individual resource:

For example, if you are calling store.findAll('user'), you can override normalizeFindAllResponse().

// app/serializers/user.js
export default DS.RESTSerializer.extend({
  normalizeFindAllResponse(store, primaryModelClass, payload, id, requestType) {
    payload.users.forEach((user) => {
      user.links = {
        pets: `/api/v1/users/${user.id}/pets`,
        company: `/api/v1/users/${user.id}/company`
      };
    });

    return this._super(...arguments);
  }
});

Here I have created a model-specific serializer to add links to each user resource. You could probably make this a little more dynamic and use it across the board in an application serializer. I’ll leave that to you.

Interested in learning more about Ember Data and how to use it with any API? Check out my book Ember Data in the Wild - Getting Ember Data to Work With Your API.


Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.

comments powered by Disqus