Customizing Relationship Links with JSON API

Not all APIs are in line with Ember Data’s adapters and serializers “standard” or “out of the box” behavior.

How do we deal with nested or filtered resources such as /post/1/comments or /api/branches?bankId=322&list=true?

Today we’ll override JSONAPISerializer and use the links property to load hasMany relationships from custom endpoints.

Example URL #1: /posts/:id/comments

A typical, “standard” blog backend with posts and comments would respond the following JSON API for a post request:

{
  data: {
    id: "1",
    type: "posts",
    attributes: { ... },
    relationships: {
      comments: {
        data: [
          { id: "1", type: "comments" },
          { id: "2", type: "comments" }
        ]
      }
    }
  }
}

This gives Ember Data a hint as to which comments to load. By default, it will hit /comments/1 and /comments/2 to satisfy the hasMany comments association. (Unless, of course, those comments were included in the post response payload.)

But what if we have a nested API structure?

  • GET /posts
  • GET /posts/:id
  • GET /posts/:id/comments

Such is the case of our simple Mirage JSON API backend.

As far as we can see, normalization is only required for posts so we’ll use the PostSerializer. We need to:

And that is exactly what the addLinks function does below. We’ll call it from both findAll and findRecord!

// app/serializers/post.js

import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({

  normalizeFindAllResponse(store, type, payload) {
    payload.data = payload.data.map(this.addLinks);
    return payload;
  },

  normalizeFindRecordResponse(store, type, payload) {
    payload.data = this.addLinks(payload.data);
    return payload;
  },

  addLinks(post) {
    post.type = 'post';
    delete post.relationships.comments.data;
    post.relationships.comments.links = {
      related: `/posts/${post.id}/comments`
    };
    return post;
  }

});

And that does the trick! Would you like to see a working example? Check out https://github.com/frank06/blog-client

Hold on a second! Aren’t adapters suppose to figure out location while serializers translate the payload?

That’s exactly right. In this case, as location is construed from the payload, we use a serializer for the customization.

Example URL #2: /comments?postId=:id

Imagine our post API was slightly different. It doesn’t return any information about comments, but it is known that a post’s comments can be accessed at /comments?postId=:id.

The serializer would end up being:

// app/serializers/post.js

import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({

  normalizeFindAllResponse(store, type, payload) {
    payload.data = payload.data.map(this.addLinks);
    return payload;
  },

  normalizeFindRecordResponse(store, type, payload) {
    payload.data = this.addLinks(payload.data);
    return payload;
  },

  addLinks(post) {
    post.relationships.comments.links = {
      related: `/comments?postId=${post.id}`
    };
    return post;
  }

});

These modifications can similarly be applied to RESTSerializer, ActiveModelSerializer and others.

For an in-depth understanding of adapters and serializers, see Fit Any Backend Into Ember with Custom Adapters & Serializers.

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)