Updating Relationships Independently in JSON:API with Ember Data

Last reviewed on October 28, 2016

In JSON:API, relationships can be modified when updating resources. For example, if you wanted to add or remove tags from an article, you could make a PATCH request to /articles/:id with the following request payload:

{
  "data": {
    "type": "articles",
    "id": "1",
    "attributes": {
      "title": "Updating Relationships Independently with Ember Data and JSON:API"
    },
    "relationships": {
      "tags": {
        "data": [
          { "type": "tags", "id": "2" },
          { "type": "tags", "id": "3" }
        ]
      }
    }
  }
}

The tags listed under relationships will replace every tag for article 1. This is the default behavior in Ember Data when updating models. The code to trigger this request payload might look something like:

model.get('tags').pushObject(tag); // tag = an existing tag record
model.save();

Did you know that JSON:API also supports updating relationships independently at URLs from relationship links? Instead of sending the associated tags in an article resource, we could make a PATCH request to /articles/1/relationships/tags that only contains the tags. For example:

{
  "data": [
    { "type": "tags", "id": "2" },
    { "type": "tags", "id": "3" }
  ]
}

To clear every tag for article 1, we can similarly do:

{
  "data": []
}

You can learn more about updating relationships in JSON:API in the spec.

How can we do this in Ember Data? It turns out, the model's save() method can take in an options object with a key called adapterOptions, which gets set on the snapshot that is passed to both the adapter and serializer.

With adapterOptions, we can pass in a flag so that we can change the URL in the adapter and serialize the request payload in the serializer accordingly. For example, instead of model.save(), we could do:

model.save({
  adapterOptions: { updateRelationship: 'tags' },
});

You can create whatever key you want in adapterOptions, but I've called it updateRelationship and set its value to "tags".

First, let's change the URL from /articles/:id to /articles/:id/relationships/tags by overriding the urlForUpdateRecord() method.

app/adapters/article.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  urlForUpdateRecord(id, modelName, snapshot) {
    let originalUpdateURL = this._super(...arguments);
    let { adapterOptions } = snapshot;
    if (adapterOptions && adapterOptions.updateRelationship === 'tags') {
      return `${originalUpdateURL}/relationships/tags`;
    }

    return originalUpdateURL;
  },
});

We'll check if adapterOptions is available on the snapshot and change the URL accordingly.

Next, let's change the request payload:

app/serializers/article.js
import DS from 'ember-data';

export default DS.JSONAPISerializer.extend({
  serialize(snapshot) {
    let serialized = this._super(...arguments);
    let { adapterOptions } = snapshot;
    if (adapterOptions && adapterOptions.updateRelationship === 'tags') {
      return serialized.data.relationships.tags;
    }

    return serialized;
  },
});

Because serialize() already serializes the relationship, we can call the original behavior and pick off the serialized relationship data if adapterOptions.updateRelationship was supplied.

If the API returns a 204 No Content response without a response document, we're done! If there is a response document, then you'll have to normalize it to the article resource or have the promise resolve with null in the adapter.

app/adapters/article.js
import DS from 'ember-data';

export default DS.JSONAPIAdapter.extend({
  // ...
  updateRecord(store, type, snapshot) {
    let promise = this._super(...arguments);
    let { adapterOptions } = snapshot;
    if (adapterOptions && adapterOptions.updateRelationship) {
      return promise.then(() => {
        return null;
      });
    }
    return promise;
  },
});

For the purposes of this post, I've hardcoded "tags" in the adapter and serializer, but you could make this more generic for updating other relationships independently and use this approach in your application adapter and serializer.

One last thing. I've found my calling code to be slightly less readable with the following:

model.save({
  adapterOptions: { updateRelationship: 'tags' },
});

Instead, I'd prefer something like:

model.save('tags');

To achieve this, we can override save() in the model:

import DS from 'ember-data';

export default DS.Model.extend({
  title: DS.attr('string'),
  tags: DS.hasMany(),
  save(relationshipKey) {
    if (typeof relationshipKey === 'string') {
      let adapterOptions = {
        updateRelationship: relationshipKey,
      };
      return this._super({ adapterOptions });
    }

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

Pro Ember Data cover
Pro Ember Data available now!

Are you struggling with Ember Data?

In my new book Pro Ember Data, you will learn how to work with Ember Data efficiently, from APIs, adapters, and serializers to polymorphic relationships, using your existing JavaScript and Ember knowledge. This book will teach you how to adapt Ember Data to fit your custom API.

What You'll Learn

  • Review the differences between normalization and serialization
  • Understand how the built-in adapters and serializers in Ember Data
  • Understand how the built-in adapters and serializers in Ember Data work
  • Customize adapters and serializers to consume any API and write them from scratch
  • Handle API errors in Ember Data
  • Work with the Reddit API using Ember Data
  • Learn how to use polymorphic relationships
Check out Pro Ember Data on Amazon using my affiliate link
You can also find Pro Ember Data on Apress.

I've been enjoying @iamdtang's Pro Ember Data book. It's to the point and has lots of great examples. My favorite part was about error handling. This book is perfect after you've exhausted the official guides.

Ilya Radchenko

Product Developer at Applied Geographics

Ilya Radchenko

Pro Ember Data is such an approachable book @iamdtang! Good job. Loving the book so far! Thank you!

Lenora Porter

Software Engineer at Heroku

Lenora Porter

Great to see all those topics in which I struggled when I started working on Ember. Nice job, will check it out :)

AbulAsar S

Fullstack Developer

AbulAsar S

I haven't read David's new book, but his previous Ember Data book helped me a lot when I was starting out with Ember, so I am sure this book is a must if you're working with Ember Data!

Kenneth Larsen

Ember Learning Core Team member

Kenneth Larsen

I bought Ember Data in the Wild back in 2016 and found it really helpful. Looking forward to reading this.

@EmberLinks

Ember related news by Chris Masters

@EmberLinks