Load Ember Data models from a non-REST API

Last reviewed on June 3, 2019 with Ember Octane

Seldom are our Ember apps perfectly aligned with a RESTful API. Often times we need to make requests to complex non-standard URLs in our backends. For example, a personalized list of tasks for a given user at /api/users/:userId/current.

Where do we start building the URL to invoke the correct API call? And more specifically – what is the proper way to load Ember Data Task models?

JSON into the Store?

When things get hairy with Ember Data, we all know we can appeal to the ultimate request tool: AJAX!

// in some route

model() {
  return axios.get(`/api/users/${someUserId}/current`);
}

And that should be it!

BUUUT… this JSON data will not populate the Store with models!

We would need to insert them into the store like this:

// in some route

async model() {
  const tasks = await axios.get(`/api/users/${someUserId}/current`);
  const ids = tasks.map(task => {
    this.store.pushPayload('task', task);
    return task.id;
  });
  return this.store.peekAll('task').filter(task => ids.includes(task.id));
}

But there is a more elegant and correct way.

Working with Ember Data, not against it

In order to retrieve a specific subset of models, Ember Data gives us the query function:

// in some route

model() {
  return this.store.query('task', { userId: someUserId });
}

As a great plus, we get all configuration (namespace, custom headers, etc) for free when working with an adapter.

Assuming the API namespace is /api, this query will resolve to an URL like /api/tasks?userId=1. But that is not at all what we need! Our backend endpoint listens at /api/users/:userId/current.

Note: If you need to apply a query-param-based filter to an association instead, read Adding query parameters to a hasMany relationship.

So let’s instruct Ember Data to:

  • given a query to Task
  • whenever there is a parameter userId
  • it should rewrite the URL to /api/users/:userId/current

Translating these requirements into code-speak:

  • in our Task adapter we will override the urlForQuery function
  • if query.userId is supplied return /api/users/${query.userId}/current
  • otherwise execute the default behavior
// app/adapters/task.js

import ApplicationAdapter from './application';

export default class TaskAdapter extends ApplicationAdapter {

  urlForQuery(query) {
    if (query.userId) {
      return `${this.urlPrefix()}/users/${query.userId}/current`;
    }
    return super.urlForQuery(...arguments);
  }

}

urlForQuery is the correct place to manipulate the string for a query URL in any way we want.

For a longer article about these kind of customizations, see Fit Any Backend Into Ember with Custom Adapters & Serializers.

Enjoyed this article? Join Snacks!

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