Last reviewed in June 2019 with Ember Octane
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.
/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 include
d in the post response payload.)
But what if we have a nested API structure?
/posts
/posts/:id
/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:
data.relationships.comments.data
(as seen in the response above, to prevent Ember Data from calling /comments/:comment_id
)data.relationships.comments.links
that points to /posts/:id/comments
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 class PostSerializer extends ApplicationSerializer {
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!
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.
/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 class PostSerializer extends ApplicationSerializer {
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.
Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)