Fit Any Backend Into Ember with Custom Adapters & Serializers

An API is a language and, for successful communication, parties need to understand each other.

Backends (Rails, CakePHP, Django, Express, or any other) often dictate the rules and styles of this language. They define an API and it’s the client’s job to adhere to it – even with a common “alphabet” like JSON.

In this guide we will explore Ember Data strategies that aid us in the translation process.

Location versus content

There are two parts to a data translation:

In the Republic of Ember, endpoint locations are translated in an Adapter. Content is translated in a Serializer.

The word resource is crucial here: if your API is not exposing resources equivalent to models in Ember, Ember Data is not the solution you are looking for.

Analogous to calling someone in the Netherlands, the Adapter would serve to prepend the +31 calling code to our destination. The Serializer itself would be the required Dutch-to-English live interpreter, even when both languages share the same Latin alphabet.

For both Adapter and Serializer this is the adapter pattern at work.

Design patterns are nice and all. But how do I fit my backend API into Ember?

What is the best way of working with (not around) Ember Data?

Which methods or hooks should we override? Can I use my ajax?

A brief overview

Ember Data’s native format is JSON API. This means that, by way of one or more serializers, everything ultimately gets translated into JSON API.

Let’s have a sneak peek at Ember Data’s handful of adapters and serializers.

Adapters

DS.RESTAdapter

Base adapter implementation. Extends the abstract class DS.Adapter. (A wildly non-standard API may warrant subclassing that one!)

The default URL mapping for RESTAdapter goes something like this:

Typically overriden methods are namespace, host, headers and buildURL which (you guessed) come in handy to define the location of our backend resources. For instance:

// app/adapters/application.js

export default DS.RESTAdapter.extend({
  host: 'https://backend',
  namespace: '/api/v2'
});

This is how our previous example would be modified with the customization above:

Both adapters and serializers use naming conventions to locate resources, here is how to customize them.

DS.JSONAPIAdapter

A subclass of DS.RESTAdapter that adjusts a few knobs for JSON API such as the Accept: application/vnd.api+json header required by the spec.

Serializers

DS.JSONSerializer

This is the base serializer that extends the abstract class DS.Serializer.

It expects this classic form of JSON:

{
  id: 1,
  name: 'Sebastian',
  friends: [3, 4]
}

It provides lots of hooks to customize the serialization process, such as keyForAttribute and keyForRelationship.

For response data massaging, we have at our disposal:

Each of the normalize methods will be invoked upon that specific action. They all share the same interface with the signature function (store, primaryModelClass, payload, id, requestType) and must return a JSON API document – which is what the Ember Data store always expects.

Yes, even when overriding JSONSerializer we must return a JSON API document. This is exactly why we would call this._super(store, primaryModelClass, payload) in such a serializer: JSONSerializer already knows how to convert a “classic JSON” payload into JSON API.

When having to adapt data submitted to the server (PUT, PATCH, POST) we’d use:

More about these later.

DS.RESTSerializer

This one extends DS.JSONSerializer in order to accommodate for a similar kind of JSON (with a “root”):

{
  person: {   // or "people: []"
    id: 1,
    name: 'Sebastian',
    friends: [3, 4]
  }
}

DS.JSONAPISerializer

Lastly, the JSON API serializer also extends DS.JSONSerializer but expects a compliant JSON API payload.

The add-on active-model-adapter is designed to integrate with Rails API style (snake case, embedded records).

Rails’ active_model_serializers also supports JSON API. We put it to use here.

“While using JSONSerializer, RESTSerializer and ActiveModelSerializer is not deprecated, we consider JSON API to be the happiest of the happy paths for using Ember Data, and if you are creating a new app today and have control over the API you should be using JSON API, because it is a well designed and comprehensive solution for JSON serialization.”
http://emberjs.com/blog/2015/06/18/ember-data-1-13-released.html

“By default Ember Data recommends using the JSONAPISerializer
http://emberjs.com/api/data/classes/DS.JSONSerializer.html

Mix ‘n match

Not only can we mix and match adapters and serializers to whatever fits best our data and URL endpoint structure. Each of these can be applied on a per-application or per-model basis.

Moreover, as we saw before, each one of these can target a specific operation via the normalize methods.

Overriding a query response for an Item model, for example, is straightforward:

// app/serializers/item.js

export default DS.JSONSerializer.extend({

  normalizeQueryResponse(store, clazz, payload) {
    payload.meta.queriedAt = +new Date();
    return this._super(store, clazz, payload);
  }

});

So which adapter/serializer should we pick?

The most closely aligned with your API.


Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

A Github client

We love building stuff, don’t we?

Let’s create a basic Github client that displays… Github stuff. Like, users!

What public info do we have for, say, wycats?

$ curl -s https://api.github.com/users/wycats
# abbreviated snippet!
{
  "id": 4,
  "name": "Yehuda Katz",
  "company": "Tilde, Inc.",
  "blog": "http://yehudakatz.com",
  "location": "San Francisco",
  "public_repos": 177,
  "public_gists": 735,
  "followers": 6564
}

Neat! We already can spot that the response is a good fit for the JSONSerializer. (See why? It’s the most closely aligned.)

Now, let’s create our app with a User model. Then start the server:

$ ember new github
$ cd github
$ ember generate route application
$ ember generate model user name publicRepos:number
$ ember server

The idea here is to treat Github as our backend. We’ll start by retrieving a user as our route model:

// app/models/user.js

export default Model.extend({
  name: attr(),
  publicRepos: attr('number')
});
// app/routes/application.js

export default Ember.Route.extend({
  model() {
    return this.store.findRecord('user', 'frank06'); // or any other user
  }
});

To be able to display his name and amount of public repositories on screen.

{{!-- app/templates/application.hbs --}}

<strong>{{ model.name }}</strong> (total {{ model.publicRepos }})

Browse to http://localhost:4200/ and all set!

All set? We see a big fat error in the console: GET http://localhost:4200/users/frank06 404 (Not Found).

A certainly expected one!

The magic adapting sauce

Ready to get our hands dirty translating Github’s API? Let’s go!

$ ember generate adapter application
$ ember generate serializer application

Task #1: Adapter

Point to the API location:

// app/adapters/application.js

import RESTAdapter from 'ember-data/adapters/rest';

export default RESTAdapter.extend({
  host: 'https://api.github.com'
});

Why RESTAdapter over the default JSONAPIAdapter?

Github is clearly not JSON API compliant, so we chose the more generic REST adapter.

Task #2: Serializer

Customize our serializer so that name and public_repos can be mapped to our User model.

As we saw earlier, the natural choice for this API seems to be JSONSerializer.

Github, as most APIs built on Rails, will return attributes in snake_case. We make use of the keyForAttribute hook to match this specific case. We will decamelize the key, which means that publicRepos will be mapped to public_repos.

// app/serializers/application.js

import Ember from 'ember';
import JSONSerializer from 'ember-data/serializers/json';

export default JSONSerializer.extend({

  keyForAttribute(key) {
    return Ember.String.decamelize(key);
  }

});

Reload the app, and… there is a slight problem!

Why two users?

Remember our request was findRecord('user', 'frank06')? It means we want a record with id=frank06 but Github returns a numeric id like 66403. We somehow need to tell Ember Data that the id or primary key is actually the login attribute.

Luckily, the primaryKey serializer property exists for this exact purpose. This, however, is very specific to the User model.

Time to put an Ember Data per-model customization to use:

$ ember generate serializer user

Anything we override in this particular serializer will only apply to the User model! To leverage common customizations (in app/serializers/application.js), we simply extend from it:

// app/serializers/user.js

import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({
  primaryKey: 'login'
});

Navigate to http://localhost:4200/… works like a charm!

There’s more than one way to skin a cat!

We could have overriden normalizeFindRecordResponse (as findRecord is the operation) instead, and return pure JSON API to the store:

// app/serializers/user.js

import JSONSerializer from 'ember-data/serializers/json';

export default JSONSerializer.extend({

  normalizeFindRecordResponse(store, type, payload) {
    return {
      data: {
        id: payload.login,
        type: type.modelName,
        attributes: {
          name: payload.name,
          publicRepos: payload.public_repos
        }
      }
    }
  }
});

It produces the exact same result!

Adding relationships

Next step is adding a relationship: user haz-lots repository.

$ ember g model repository fullName language stargazersCount:number

The generated Repository model:

// app/models/repository.js

import Model from 'ember-data/model';
import attr from 'ember-data/attr';

export default Model.extend({
  fullName: attr(),
  language: attr(),
  stargazersCount: attr('number')
});

The updated User model:

// app/models/user.js

export default Model.extend({
  name: attr(),
  publicRepos: attr('number'),
  repositories: hasMany()
});

Let’s have a look again at the classic JSON form:

{
  id: 1,
  name: 'Sebastian',
  friends: [3, 4]
}

A person with many friends, would have its friends ids in an array. Bringing it to our User/Repository example, we’d expect Github to include them:

{
  login: "frank06",
  id: 66403,
  // ...
  repos: [5620, 1103, 780003]
}

Alas, it does not…

However, Github includes a repos_url that points to repositories! Let’s update our serializer to load repos from a link:

// app/serializers/user.js

import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({  

  primaryKey: 'login',

  normalizeFindRecordResponse(store, type, payload) {
    payload.links = {
      repositories: payload.repos_url
    }
    return this._super(...arguments);
  }

});

We are basically intercepting the findRecord response and adding a links.repositories property to the payload.

Last step, update the template to show the list of repositories:

{{!-- app/templates/application.hbs --}}

<strong>{{ model.name }}</strong> (total {{ model.publicRepos }})

<ul>
{{#each model.repositories as |repo|}}
  <li>{{repo.fullName}} ({{repo.language}}, {{repo.stargazersCount}} stars)</li>
{{/each}}
</ul>

Done!

When transforming arrays and other complex datasets, it’s important to be familiar with the reduce function.

Stargazing

How about saving some data?

Unfortunately, there is not much we can save to other’s repositories.

There is an option to send data to Github, though: starring a repository. It is a little contrived (because it is not really saving a resource) but it will allow us to understand several adapter mechanisms.

Normally, starring a repository would be considered a REST action. Here is how to handle non-standard REST actions.

Get yourself a Github access token. We’ll need it for authenticating in this section.

We will add a button in the interface to star one of the listed repositories, in three steps:

#1: Update the template with a Star button

 {{!-- app/templates/application.hbs --}}

 <!-- snip -->
  <li>
   {{repo.fullName}} ({{repo.language}}, {{repo.stargazersCount}} stars)
   <button {{action 'star' repo}}>Star!</button>
  </li>
 

#2: Listen for the action in the route and call save

 // app/routes/application.js

 export default Ember.Route.extend({
   actions: {
     star(repo) {
       repo.save();
     }
   },
   // ...
 });
 

#3: Override the adapter

Let’s modify updateRecord to place our custom URL and ajax:

In a standard save/update operation, the adapter calls an appropriate serializer method (here is where the serialize methods we mentioned earlier take the stage).

Next, it builds the URL and finally executes the ajax request, returning a Promise.

This is the anatomy of a RESTAdapter’s updateRecord:

// ember-data/rest-adapter.js

updateRecord: function (store, type, snapshot) {
  var data = {};
  var serializer = store.serializerFor(type.modelName);

  serializer.serializeIntoHash(data, type, snapshot);

  var id = snapshot.id;
  var url = this.buildURL(type.modelName, id, snapshot, 'updateRecord');

  return this.ajax(url, "PUT", { data: data });
},

Again, we create a repository adapter since this is specific to Repository.

$ ember g adapter repository

In our example, we’ll hijack the adapter’s updateRecord to send an empty PUT to the “star” Github endpoint (/user/starred/:owner/:repo). Adding to the headers the previously obtained authorization token:

// app/adapters/repository.js

import ApplicationAdapter from './application';

export default ApplicationAdapter.extend({

  headers: {
    Authorization: "token v3ryl0ngt0k3nfr4mg1thub" // replace!
  },

  updateRecord(store, type, snapshot) {
    const url = `${this.host}/user/starred/${snapshot.attr('fullName')}`;
    return this.ajax(url, "PUT", {});
  }

});

Try starring a repo!

Wrapping up

This guide is by no means exhaustive! There are countless combinations of adapters & serializers. I hope the examples on this guide were useful for your own translation strategy.

Yes, this project is on Github. It would be very meta if you starred it through the example app. :)

Was this actually helpful to you? Did I miss anything you believe is important? Were you able to successfully run the example? Let me know in the comments below!

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!)