Ember-CLI-Mirage: Let’s build a kick-ass server and mock database 🏄

Adam Skoczylas
6 min readNov 18, 2019

Working with Ember.js, I consider ember-cli-mirage to be an essential library for writing tests and developing new features. Mirage is the perfect tool to get your app running when back-end work is in progress, test environment servers are down, or to write meaningful integration and acceptance tests using mock data 🛹. But as always, there is a bit of a learning curve for inexperienced users. In this article, I’ll explain how to setup and use ember-cli-mirage to generate mock server data so you can implement it in your app 🎈.

In the code below, I’ll use a simple app that we created in a previous article about ember-data. To follow along, feel free to clone this GitHub repository:

https://github.com/thisFunction/poke-app

Photo by Victor Freitas on Unsplash

First things first! 🚀

We need to install the ember-cli-mirage library in our ember app:

ember install ember-cli-mirage

Done! 💪 Now, there are three things we want to do before working with Mirage.

First, in the /tests folder, create a .eslintrc.js file. In this file, add the following code to avoid a 'server' is not defined ESLint error:

module.exports = {
env: {
embertest: true,
},
globals: {
server: true,
}
};

Second, let’s switch our app from using the PokéAPI back-end to Mirage. This is done by adding the following code in the /config/environment.js file:

if (environment === 'development') 
ENV['ember-cli-mirage'] = {
enabled: true,
};

}

Third, because our back-end doesn’t use JSON API 😥, we need to change our Mirage application serializer to use a REST serializer. This is easily done by changing the contents of /serializers/application.js file to this:

import { RestSerializer } from 'ember-cli-mirage';export default RestSerializer;

Congratulations, you are now using Mirage! Unfortunately, if you reload your app, it is 100%… broken. 😬

Let the fun begin! 🤹🏼‍♂

For our application to work, there are a few things that we need to configure in Mirage. Essentially, we need to make sure that Mirage intercepts all the API calls that go out to pokeapi.co and returns mock data instead.

Since we are using PokéAPI for our back-end, we need to add the following code in the /mirage/config.js file:

export default function() {
this.urlPrefix = 'https://pokeapi.co';
this.namespace = 'api/v2';
this.timing = 400;
}

The urlPrefix and namespace properties are used to build the back-end URL and come directly from our /app/adapters/pokemon.js file. We’ll use the default timing, it defines how fast or slow we want Mirage to serve us data.

In this file, we also need to define what happens when our Mirage server gets a request to a route in the URL and namespace we defined. For example, in order to handle a PUT request to https://pokeapi.co/api/v2/pokemon we need to add this.put('pokemon') to our config file. If we had a DELETE request, we would need to define this.delete('pokemon'). Here is a handy link to all the different shorthand route handlers.

In our app, we only do two requests. Both are GET requests to /pokemon route, the difference is that one is sent with an id parameter. In our config file, we can specify both like this:

export default function() {
this.urlPrefix = 'https://pokeapi.co';
this.namespace = 'api/v2';
this.timing = 400; t
this.get('/pokemon');
this.get('/pokemon/:id');
}

Now, if we refresh our app, we see Error: poke-app/mirage/factories/pokemon-detail must export a factories 🙀.

Lets investigate 🧐

Our app uses Mirage as back-end… ✅

Mirage intercepts our GET requests… ✅

Mirage returns mock data to the app… ❌

We pinpointed the problem, now let’s define a Mirage factory to let us build the data we want it to return.

Factories build mock data 🏭

We’ll begin with generating a mirage factory:

ember g mirage-factory pokemon-detail

Next, in the /mirage/factories/pokemon-detail file, we can get as creative 🧶 or boring 💤 as we want, as long as we define the same properties that we have in our pokemon-detail model. Here is my approach in generating name, height, weight, and sprites properties using faker.js:

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';
const randomEnding = faker.random.arrayElement(["mon","chu","saur", "lett", "ite", "lax"]);export default Factory.extend({
name() {
return `${faker.random.word()}${randomPokemonNameEnding}`;
},
height() {
return faker.random.number();
},
weight() {
return faker.random.number();
},
sprites() {
return {
back_default: "https://via.placeholder.com/100",
back_shiny: "https://via.placeholder.com/100",
front_default: "https://via.placeholder.com/100",
front_shiny: "https://via.placeholder.com/100"
}
}
});

The last step that we need to take is tell mirage how many times we want this factory to build data. Since we display our Pokémon in groups of 20, let’s create 60 database posts. That way, we can see our before and next page buttons in action.

To do this, add the following line in /mirage/scenarios/default.js:

export default function(server) {
server.createList('pokemon-detail', 60);
}

Refresh our app, and… aha! We see the same console error, only this time it’s for our other route 😏. Since we already know what to do, so let’s get to it!

ember g mirage-factory pokemon

This file, /mirage/factories/pokemon, will be a little bit more complicated, due to the fact that we are expecting an array of 20 Pokémon name and URL objects per request. Click here to see the actual API response. Also, we need to use an afterCreate method to update the total Pokémon count in each previously created database entry, because we expect this number to be the same very time we make a request. Take a look:

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';
const pokemonPerPage = 20const randomEnding = faker.random.arrayElement(["mon","chu","saur", "lett", "ite", "lax"]);export default Factory.extend({
count(i) {
return (i + 1) * pokemonPerPage;
},
results(i) {
let pokemonCreated = 1;
let pokemonResults = [];
while(pokemonCreated <= pokemonPerPage) {
const pokemonId = (i * 10) + pokemonCreated;
const pokemonObject = {
name: `${faker.random.word()}${randomEnding}`,
url: `https://pokeapi.co/api/v2/pokemon/${pokemonId}`
}
pokemonResults.push(pokemonObject);
pokemonCreated += 1;
}
return pokemonResults;
},

afterCreate(post, server) {
server.db.pokemons.update({count: post.count})
}
});

In the count method, we create a total database entries count based on the number of Pokémon we want to display per page (20) and the number of times this factory is running.

The results method crates an array of 20 objects containing a name and URL.

The afterCreate method takes the Pokémon count from this factory iteration and updates all entries in the database to have the same number. Mirage provides an handy update method to update all entries in a specified database collection.

That was fun! 🤸🏽‍♀️ Let’s not forget to update our /mirage/scenarios/default.js file and create this data three times:

export default function(server) {
server.createList('pokemon', 3);
server.createList('pokemon-detail', 60);
}

After restarting the app, all console errors are gone! Huzzah! 🥳 But if we look at the content of our app, it’s… completely empty 😞.

WTF!? 🤷🏽‍♂️

Let’s put a debugger in the /mirage/scenarios/default.js

export default function(server) {
server.createList('pokemon', 3);
server.createList('pokemon-detail', 60);
debugger
}

If we console.log server.db we see that our factories are creating data as expected: pokemonDetails: Array(60), pokemons: Array(3).

This means that Mirage is not sending data in the same format that out app is expecting it. If we debug the normalizeResponse method in the /app/serializers/application.js file, we see that the payload is an array of three objects. Our app, however, expects to get this data one object at a time.

Final fixes 🔧

In the /mirage/config.js file, we need to add a bit of logic to our GET requests, so Mirage sends our mock data the way ember data expects it. Since PokéAPI uses pagination, we need to implement something similar. Here is my ad hoc approach:

export default function() {
this.urlPrefix = 'https://pokeapi.co';
this.namespace = 'api/v2';
this.timing = 400;
this.get('/pokemon', (schema, request) => {
const offset = Number(request.queryParams.offset);
return schema.db.pokemons[offset/20];
});
this.get('/pokemon/:id', (schema, { params }) => {
return schema.db.pokemonDetails.find(params.id);
});
}

For the first GET request, we will use the offset query parameter to serve the correct database record. Dividing the offset by 20 (Pokémon per page) we get the index of the database entry that we want. Piece of cake 🍰!

To receive the correct Pokémon details data, we can use the id parameter to find the corresponding database entry. Mirage provides a helpful find method to make this super easy 🍋!

Now we reload our app and… we are golden! 🏆 Our app is now running completely off of Mirage. This is super useful in a number of cases and will become the backbone for writing meaningful integration and acceptance tests for our Ember app.

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

--

--