Ember-data: Turn Frustration 🤬 into Celebration 🎉

May 21 · 10 min read

Ember-data is a powerful tool that allows ember.js developers to manage API calls and deal with the state on the front-end side. For inexperienced users, it can also be a source of great frustration. In this article, we will build a simple web app that uses ember-data to fetch and display back-end data and address some of the issues that we will encounter along the way.

Photo by Matthew Payne on Unsplash

Gotta catch `em all 🌞

You’re assigned a task to create an app that retrieves data from pokeapi.co and displays it for the user. Your only requirements are to use ember.js and ember-data. Sounds simple, right?

I have created a GitHub repository called poke-app if you’d like to follow along https://github.com/thisFunction/poke-app, and you can see the app in action on Heroku.

Getting started 🐣

Before we start coding, let’s write down some requirements for this project.

Our app will have two routes:

  1. pokemon route: this route will show a total count of Pokémon available on the API, and list Pokémon names in groups of 20. This information will come from GET requests to https://pokeapi.co/api/v2/pokemon?limit=20&offset={offset}. We will use the offset query parameter to account for paginated data.
  2. pokemon-details route: this route will display a specific Pokémon’s name, height, weight, and image. This information will come from GET requests to https://pokeapi.co/api/v2/pokemon/{id}.

On your marks, get set, go! 🚴

We’ll start by generating our first route called pokemon:

ember generate route pokemon

In your router.js file, you should see the following code:

Router.map(function() {
this.route('pokemon');
});

Next, we will create the second, pokemon-details, route. This will be a nested route, and its path will be pokemon/pokemon-details.

ember g route pokemon/pokemon-details

Your router.js file will now look like this:

Router.map(function() {
this.route('pokemon', function() {
this.route('pokemon-details');
});
});

Because the pokemon-details route will have a dynamic URL, we need to add a path option, {path: '/pokemon_id}, in our router.js file. It’s also good practice for our URLs to reflect the RESTful API resource naming conventions, so that’s why we’ll omit /pokemon-details/ in our URL and use slash instead.

Here is the whole route defined in our router.js file:

Router.map(function() {
this.route('pokemon' , function() {
this.route('pokemon-details', { path: '/:pokemon_id' });
});
});

With these changes, our nested route is now able to display different information based on a Pokémon’s id. 🤩

So far so… 🙀

If we open up our app and navigate to http://localhost:4200/pokemon or http://localhost:4200/pokemon/2, we see that our app renders our empty route templates, but in our console, we see this error:

message: "No model was found for 'pokemon'"

Since we added a dynamic path /:pokemon_id to our pokemon-details route, ember-data will look for a model containing this id to generate the desired route.

Ember CLI to the rescue! 🚀 We need to generate two models. Let’s do this in one shot:

ember g model pokemon && ember g model pokemon-detail

In our models folder, we get two new files: pokemon.js and pokemon-detail.js. In these files, we need to add all the properties that ember-data will receive from the PokeAPI server so we can display them in our app.

In our pokemon route, we are interested in the total Pokémon count, as well as the results array. You will notice that the PokeAPI also provides us with a next and previous link, but we will implement our own solution for those. ⚡

In our models/pokemon.js file, we need to add the following code:

import DS from 'ember-data';export default DS.Model.extend({
count: DS.attr(),
results: DS.attr(),
});

In our pokemon-details route, we are interested in displaying a specific Pokemon’s name, height, weight, and image. So we need to make changes to our models/pokemon-detail.js file and include those four properties:

import DS from 'ember-data';export default DS.Model.extend({
name: DS.attr(),
height: DS.attr(),
weight: DS.attr(),
sprites: DS.attr()
});

Adapters: WTF!? 😵

When we navigate to our routes again, we get… more errors! 😫

Our pokemon route seems OK, although it doesn’t render any data yet, pokemon-details throws this error message in our console.

Error while processing route: pokemon.pokemon-details
Ember Data Request GET /pokemons/2 returned a 404 Payload

If you take a peek at your developer tools network tab, you will see that there was a GET request made to http://localhost:4200/pokemons/2. Wow! That’s pretty cool! 😎 Now, if there only was a way to tweak that request URL and point it in the right direction…

Ember CLI to the rescue! 💪 Let’s generate a pokemon adapter to help ember-data request information from the correct place.

ember generate adapter pokemon

In your adapters folder, you will see a new file called pokemon.js. In this file, we need to make a few changes. First of all, ember-data assumes that our back-end uses the JSON:API standard. When you read the first sentence on PokeAPI page, however, you will know that it is a RESTful API. Whenever you’re not sure what standard your API is written in, can just take an example response from the back-end and use a JSON:API validator to help you out.

Now, in the adapters/pokemon.js file, we need to change this code:

export default DS.JSONAPIAdapter.extend({
});

with this:

export default DS.RESTAdapter.extend({
});

Next, we need to make sure that ember-data knows where to make the GET request. For this, we will use the host and namespace properties. These values come directly from the PokeAPI documentation.

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

Almost there 😅

If we refresh our route, we see that the request was made to https://pokeapi.co/api/v2/pokemons/2. By default, ember-data pluralizes the type’s name, so pokemon becomes pokemons. 🤭 No worries, let’s just add the pathForType method in our adapter, and return our model name as we have defined it, i.e. “pokemon”.

Our entire file now looks like this:

import DS from 'ember-data';export default DS.RESTAdapter.extend({
host: 'https://pokeapi.co',
namespace: 'api/v2',
pathForType(modelName) {
return modelName;
}
});

We refresh our page, and … TA-DA! In our network tab, we get a happy 200 status response, along with a shiny new payload. 🤙 Thanks, ember-data! That was pretty cool, but let’s see more ember-data magic! 🧝🏽

Fetching all the data 🚚

We started working on our API calls starting with the pokemon-details route. Now let’s tie it all together, and work on the pokemon route.

For this, we need to give the pokemon route a model to display. Let’s add a model method in our routes/pokemon.js file. Here we will use the queryRecord to fetch a record from the API and include the offset parameter to help us with pagination.

import Route from '@ember/routing/route';export default Route.extend({
queryParams: { offset: { refreshModel: true }},

model(params) {
const offset = params.offset ? params.offset : 0;

return this.store.queryRecord('pokemon', {
offset
});
}
});

If we look in the developer tools network tab, we see that we get a payload and a happy 200 status from the server. 🎂 Time to use that data in our templates.

Displaying a list of Pokemon 👻

If we inspect the payload that we receive from PokeAPI in our pokemon route, you will see that it is a flat structure JSON. It looks like this:

{
"count": 964,
"next": "https://pokeapi.co/api/v2/pokemon?offset=20&limit=20",
"previous": null,
"results": [
{
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon/1/"
} …
]
}

Ember-data, however, expects our data to be structured a certain way. In fact, it uses JSON:API underneath to make sense of all the data it receives.

Serializers to the rescue! 🙌

This is where serializers come into play. In a gist, serializers allow us to format, or “normalize”, the payload we receive from the back-end to reflect the structure ember-data expects.

First, let’s generate a pokemon serializer:

ember g serializer pokemon

In our pokemon serializer, we will normalize the JSON payload so that all the properties we receive from the back-end are nested inside a pokemon object.

Basically, we need the data to look like this:

{
"pokeomon": {
"count": 964,
"next": "https://pokeapi.co/api/v2/pokemon?offset=20&limit=20",
"previous": null,
"results": [
{
"name": "bulbasaur",
"url": "https://pokeapi.co/api/v2/pokemon/1/"
} …
]
}
}

In our serializers/pokemon.js file, we need to change DS.JSONAPISerializer to DS.RESTserializer. Then, we’ll use the normalizeResponse method to structure our payload. Also, since we used the queryRecord method to fetch data from the API in our pokemon route, ember-data expects an id with each payload. A simple solution that we’ll use here is Date.now(). In production code, I would use the offset parameter value, but that’s a bit out of scope for this article. 😉

Let’s have a look at the code:

import DS from 'ember-data';export default DS.RESTSerializer.extend({
normalizeResponse(store, model, payload, id, requestType) {
const payloadId = Date.now();
payload = {
pokemon: {
id: payloadId,
results: payload.results,
count: payload.count
}
}
return this._super(store, model, payload, id, requestType);
}
});

Basically, we have one main level pokemon property, which is also our model, and everything else nested inside will be used by that model, just like we defined it in our models/pokemon.js file. 🤘

Back to templates 💃🏾

Rendering our app in the browser, we see that all errors are gone form the console, and if you have ember-inspector installed, you can check that ember-data has the correct data for our pokemon model type!

To render this on our template, we need to use the model property and loop over the results array.

<h1>Pokemon</h1><p>Pokemon on PokeAPI:</p>
{{model.count}}
<p>Results:</p>
<ul>
{{#each model.results as |result|}}
<li>{{result.name}}</li>
{{/each}}
</ul>

Next, we will use the ember template link-to helper to redirect the app to the pokemon-detail pages. But first, we need to extract the pokemon id from the model.results.url property. There are many ways to go about this, one way is to create a helper to help us out. 👻

ember g helper extract-id-from-url

In the helper file, helpers/extract-id-from-url.js we will use the following code:

import { helper } from '@ember/component/helper';export function extractIdFromUrl(params) {
const [url] = params;
let pokemonId = url.replace('https://pokeapi.co/api/v2/pokemon/','').replace('/','');
return pokemonId;
}
export default helper(extractIdFromUrl);

In our templates/pokemon.hbs file, we also need to add a link-to helper with the full route path separated with periods, followed by the extracted id:

<li>{{#link-to "pokemon.pokemon-details" (extract-id-from-url result.url)}}{{result.name}}{{/link-to}}</li>

At this point, we have a list of links in our app that transition us to the pokemon-details route with a dynamically added id. 🔥

Displaying more details 🧐

When we click on a link to view more details about a specific Pokémon, we get the following error in our console:

Error while processing route: pokemon.pokemon-details 
Ember Data Request GET /pokemon-details/19 returned a 404

In the network tab, we can see that the request was made to a localhost URL:

http://localhost:4200/pokemon-details/19

Since we defined an adapter to help us guide ember-data to the correct URL for our pokemon model, let’s do the same for pokemon-details.

Ember CLI do your magic 🧙‍♀️

ember g adapter pokemon-detail

In our adapters/pokemon-detail.js file, we want to import our previous pokemon adapter and extend it with the buildURL method.

import Pokemon from './pokemon';export default Pokemon.extend({
buildURL (modelName, id) {
return `${this.host}/${this.namespace}/pokemon/${id}`;
}
});

Take a peek in the network tab. TA-DA! 👩‍🚀 We have the correct response from the server, now let’s display that data!

Here is an example of what I included in my templates/pokemon-detail.hbs file, but feel free to get creative. 💅🏽

name: {{model.name}} <br>
height: {{model.height}} <br>
weight: {{model.weight}} <br>
<img src={{model.sprites.front_default}} alt="{{model.name}} front image">
<img src={{model.sprites.front_shiny}} alt="{{model.name}} front image">
<img src={{model.sprites.back_default}} alt="{{model.name}} back image">
<img src={{model.sprites.back_shiny}} alt="{{model.name}} back image">

Final touches 🏃🏼

At this point, we have an app that displays a list of Pokémon from PokeAPI and allows us to click on each Pokémon to see its details. All we need is a way to navigate the list of Pokémon returned in groups of 20 and we’re done. 🌴

First, we’ll generate a controller for the pokemon route:

ember g controller pokemon

In controllers/pokemon.js we will add two functions that enable or disable the next and previous buttons, and two actions to transition the pokemon route to a different pagination offset.

import Controller from '@ember/controller';
import { computed } from '@ember/object';
export default Controller.extend({
queryParams: ['offset'],
offset: 0,

previousPageButtonIsDisabled: computed(
"model.offset",
function(){
return this.offset === 0 ;
}
),

nextPageButtonIsDisabled: computed(
"model.{offset,count}",
function() {
const offset = this.offset;
const itemsPerPage = 20;
const totalItemCount = this.model.count
return offset + itemsPerPage >= totalItemCount;
}
),
actions: {
nextPage() {
this.transitionToRoute('pokemon', {
queryParams: {
offset: Number(this.offset) + 20
}
});
},
previousPage() {
this.transitionToRoute('pokemon', {
queryParams: {
offset: Number(this.offset) - 20
}
});
}
}
});

All that we have left to do, is add two button HTML elements to our templates/pokemon.hbs file.

<button 
{{action "previousPage"}}
disabled={{previousPageButtonIsDisabled}}
>
Previous
</button>
<button
{{action "nextPage"}}
disabled={{nextPageButtonIsDisabled}}
>
Next
</button>

If you take a look at your app in the browser, you’ll see that now you can click the next and previous buttons for more Pokémon results, and display each Pokémon’s details by clicking on its name. Also, the next and previous buttons are disabled when we reach the beginning and end of the results list.

Conclusion 🙇🏽‍♀️

As we can see, working with ember-data can be both helpful and frustrating. Overall, I think it’s a very powerful tool with a bit of a learning curve and unfortunately not too many real-life examples. Hopefully, this article will help you solve some common problems that I have encountered along the way and encourage you not to give up. 💫

Thanks for reading!
Adam Skoczylas — ember.js developer at SoapBox

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade