Software Engineer · Teacher · Author · Vegan

Extracting Metadata from a Custom API with Ember Data

Last reviewed on January 18, 2020

When querying an API, sometimes metadata is returned such as pagination information. For example:

{
  "post": [
    { "id": 1, "title": "Post 1" },
    { "id": 2, "title": "Post 2" },
    { "id": 3, "title": "Post 3" }
  ],
  meta: {
    "total": 100
  }
}

With the response above, we can access meta off of the result of our store.query() call:

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

export default class IndexRoute extends Route {
  model() {
    return this.store.query('post', { page: 1 });
  }

  afterModel(posts) {
    console.log('Meta: ', posts.get('meta')); // { total: 100 }
  }
}

Great, but what if our API isn’t structured like above? Let’s say we have something like this instead:

{
  "total": 100,
  "items": [
    { "id": 1, "title": "Post 1" },
    { "id": 2, "title": "Post 2" },
    { "id": 3, "title": "Post 3" }
  ]
}

This response doesn’t match what any of the built-in serializers expect. If you’re not familiar with the different Ember Data serializers, check out my other post, Which Ember Data Serializer Should I Use?.

Let’s try normalizing this response for JSONSerializer:

import JSONSerializer from '@ember-data/serializer/json';

export default class ApplicationSerializer extends JSONSerializer {
  normalizeQueryResponse(store, ModelClass, payload, id, requestName) {
    return super.normalizeQueryResponse(store, ModelClass, payload.items, id, requestName);
  }
}

Cool, that works. Next, let’s extract the metadata. Looking at the API documentation for JSONSerializer, my first thought was to use the extractMeta(store, ModelClass, payload) method. I tried something like this:

import JSONSerializer from '@ember-data/serializer/json';

export default class ApplicationSerializer extends JSONSerializer {
  normalizeQueryResponse(store, ModelClass, payload, id, requestName) {
    return super.normalizeQueryResponse(store, ModelClass, payload.items, id, requestName);
  }

  extractMeta(store, ModelClass, payload) {
    console.log('payload', payload);
  }
}

Unfortunately, the payload logged to the console in extractMeta was the following:

[
  { "id": 1, "title": "Post 1" },
  { "id": 2, "title": "Post 2" },
  { "id": 3, "title": "Post 3" }
]

No sight of that total property. It turns out, extractMeta() gets called after normalizeQueryResponse(), so payload in extractMeta isn’t the original payload; it is the normalized one.

So how can we make this work?

One approach is to extend RESTSerializer instead of JSONSerializer:

import RESTSerializer from '@ember-data/serializer/rest';

export default class ApplicationSerializer extends RESTSerializer {
  normalizeQueryResponse(store, ModelClass, payload, id, requestName) {
    payload[ModelClass.modelName] = payload.items;
    delete payload.items;
    return super.normalizeQueryResponse(...arguments);
  }

  extractMeta(store, ModelClass, payload) {
    let { total } = payload;
    payload.meta = { total };
    delete payload.total;
    return super.extractMeta(...arguments);
  }
}

Here we are normalizing the payload to match what RESTSerializer expects, which involves changing the items key to post. The normalized payload gets passed into extractMeta, which still has total. We can then assign total as a property in a meta object.

A second approach is to use JSONSerializer as follows:

import JSONSerializer from '@ember-data/serializer/json';

export default class ApplicationSerializer extends JSONSerializer {
  normalizeQueryResponse(store, ModelClass, payload, id, requestName) {
    let normalized = super.normalizeQueryResponse(
      store,
      ModelClass,
      payload.items,
      id,
      requestName
    );

    normalized.meta = this.extractMeta(store, ModelClass, payload);
    return normalized;
  }

  extractMeta(store, ModelClass, payload) {
    let meta = {
      total: payload.total
    };

    return meta;
  }
}

In this implementation with JSONSerializer, the payload is first normalized into JSON:API since Ember Data uses that internally. Then, we can call extractMeta ourselves with the raw payload and assign the result as the meta property on the normalized payload.

Here is the code from this post.

Thanks to @Runspired for helping me with the JSONSerializer implementation.

Ember Data in the Wild book cover

Have a custom API that you aren't sure how to use with Ember Data? Interested in writing your own adapter or serializer? Want to just know more about how Ember Data works? Check out my book Ember Data in the Wild - Getting Ember Data to Work With Your API .


Disclaimer: Any viewpoints and opinions expressed in this article are those of David Tang and do not reflect those of my employer or any of my colleagues.