Ember-data: Turn Frustration šŸ¤¬ into Celebration šŸŽ‰

Adam Skoczylas
10 min readMay 21, 2019

--

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

--

--