Fit Any Backend Into Ember with Custom Adapters & Serializers

Last reviewed in June 2019 with Ember Octane

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

Backend APIs often dictate the rules and styles of this language 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 and content

There are two parts to data translation:

  • Location: where we look for a remote resource
  • Content: the actual resource representation

In Ember, endpoint locations are translated in an Adapter. Content is translated in a Serializer:

  • Similar to calling someone in the Netherlands, the Adapter would serve to prepend the +31 calling code to our destination.
  • Once the communication is established, 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.

So 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 internally into JSON API.

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

diagram

Adapters

RESTAdapter

Base adapter implementation. Extends the abstract class Adapter. (A wildly non-standard API may need to implement that one!)

The default URL mapping for RESTAdapter goes something like this:

  • item1.save() issues a POST on /items if new, or a PUT on /items/1 if already persisted
  • item1.destroy() issues a DELETE on /items/1

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

import DS from 'ember-data';

export default class ApplicationAdapter extends DS.RESTAdapter {
  host = 'https://backend';
  namespace = '/api/v2';
}

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

  • item1.save() issues a POST on https://backend/api/v2/items if new, or a PUT on https://backend/api/v2/items/1 if already persisted
  • item1.destroy() issues a DELETE on https://backend/api/v2/items/1
Both adapters and serializers use naming conventions to locate resources, here is how to customize that naming.

JSONAPIAdapter

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

Serializers

JSONSerializer

This is the base serializer that extends the abstract class 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:

  • normalizeResponse()
  • normalizeFindAllResponse()
  • normalizeFindRecordResponse()
  • normalizeFindManyResponse()
  • normalizeFindBelongsToResponse()
  • normalizeFindHasManyResponse()
  • normalizeQueryResponse()
  • normalizeQueryRecordResponse()
  • normalizeCreateRecordResponse()
  • normalizeDeleteRecordResponse()
  • normalizeUpdateRecordResponse()
  • normalizeSaveResponse()
  • normalizeSingleResponse()
  • normalizeArrayResponse()

Each of the normalize methods will be invoked upon that specific action. They all 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 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:

  • serialize()
  • serializeAttribute()
  • serializeBelongsTo()
  • serializeHasMany()

More about these later.

RESTSerializer

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

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

JSONAPISerializer

Lastly, the JSON API serializer also extends 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.

If you are creating a new app today and have control over the API, JSON API is considered to be “the happy path”.

“By default Ember Data recommends using the JSONAPISerializer” – https://api.emberjs.com/ember-data/release/classes/DS.JSONSerializer</small>

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

import DS from 'ember-data';

export default class ItemSerializer extends DS.JSONSerializer {
  normalizeQueryResponse(store, clazz, payload) {
    payload.meta.queriedAt = +new Date();
    return super.normalizeQueryResponse(store, clazz, payload);
  }
}

So which adapter/serializer should we pick?

The most closely aligned with your API.

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

The Github adapter

We love building stuff, don't we?

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

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

$ curl -s https://api.github.com/users/frank06
{
  "id": 66403,
  "name": "Frank Treacy",
  "blog": "https://emberigniter.com",
  "public_repos": 53,
  "public_gists": 12,
  "followers": 132,
  ...
}

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 ember-github-adapter -b @ember/octane-app-blueprint
$ cd ember-github-adapter
$ ember generate route application   # overwrite template
$ ember generate controller application   # we are going to need it
$ ember generate model user name publicRepos:number

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


import Model, { attr } from '@ember-data/model';

export default class UserModel extends Model {
  @attr name;
  @attr('number') publicRepos;
}

Troubleshooting 🚧

The blueprints for generating Ember Data model, adapter, etc are broken in Ember Data 3.11 (i.e. Unknown blueprint: model). If this is an issue for you make sure you specify "ember-data": "3.10.0", in your package.json and run npm install again.

// app/routes/application.js

import Route from '@ember/routing/route';

export default class ApplicationRoute extends Route {
  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 }})

Run ember serve and browse to http://localhost:4200/!

Blank page… AND a big fat error in the console: GET http://localhost:4200/users/frank06 404 (Not Found) 😕.

Magic adapting sauce

Ready to start translating Github's API? Let's go!

$ ember g adapter application
$ ember g serializer application

Task #1: Adapter

Point to the API location:

// app/adapters/application.js

import DS from 'ember-data';

export default class ApplicationAdapter extends DS.RESTAdapter {
  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 DS from 'ember-data';
import { decamelize } from '@ember/string';

export default class ApplicationSerializer extends DS.JSONSerializer {
  keyForAttribute(key) {
    return decamelize(key);
  }
}

Reload the app, and… there is another problem!

You do have the Ember Inspector, don't you?
You do have the Ember Inspector, don't you?

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 g 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 class UserSerializer extends ApplicationSerializer {
  primaryKey = 'login'
}

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

diagram

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 DS from 'ember-data';

export default class ApplicationSerializer extends DS.JSONSerializer {
  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, { attr } from '@ember-data/model';

export default class RepositoryModel extends Model {
  @attr fullName;
  @attr language;
  @attr('number') stargazersCount;
}

The updated User model:

// app/models/user.js


import Model, { attr, hasMany } from '@ember-data/model';

export default class UserModel extends Model {
  @attr name;
  @attr('number') publicRepos;
  @hasMany repositories;
}

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 class UserSerializer extends ApplicationSerializer {
  primaryKey = 'login'

  normalizeFindRecordResponse(store, type, payload) {
    payload.links = {
      repositories: payload.repos_url
    }
    return super.normalizeFindRecordResponse(...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!

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

Stargazing

How about saving some data?

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

Starring a repository does involve sending a PUT to the repo. For the sake of learning let's configure Ember Data such that anytime we call repository.save() it stars the repo. This will allow us to understand several adapter mechanisms.

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

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 --}}

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

 <ul>
   {{#each @model.repositories as |repo|}}
     <li>
       {{repo.fullName}} ({{repo.language}}, {{repo.stargazersCount}} stars)
       <button {{on "click" (fn this.star repo)}}>Star!</button>
     </li>
   {{/each}}
 </ul>

#2: Call save

We are using the on modifier on the button which will run the star function on click. Let's define that function on the controller:

// app/controllers/application.js

import Controller from '@ember/controller';

export default class ApplicationController extends Controller {

  star(repo) {
    repo.save();
  }

}

#3: Override the adapter

Let's modify updateRecord to place our custom URL and ajax.

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

$ ember g adapter repository

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

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 });
},

In our example, we'll hijack the adapter's updateRecord to send an empty PUT to the /user/starred/:owner/:repo endpoint. Adding to the headers the previously obtained authorization token:

// app/adapters/repository.js

import ApplicationAdapter from './application';

export default class RepositoryAdapter extends ApplicationAdapter {

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

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

}
Get your own Github access token here.

Now try starring a repo… it works!

(Results seem to be visible immediately on Github.com but can take 10-20 seconds to show up via the Github API.)

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 this sample 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? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)