Mocking a Blog Backend with Mirage and JSON API

Last reviewed in July 2019 with Ember Octane Mirage 1.0

Mirage is a fantastic mocking library. It allows us to build frontend apps really fast, without the need of a real backend API.

Today we are going to create a blog with commenting capabilities:

  • a posts listing page
  • a single post page
  • a list of comments and a textarea to add new comments

Let's first set up an Ember app:

$ ember new ember-mirage-blog -b @ember/octane-app-blueprint --no-welcome

Next, install Mirage:

$ ember install ember-fetch  # required as we don't have jquery
$ ember install ember-cli-mirage
installing ember-cli-mirage
  create /mirage/config.js
  create /mirage/scenarios/default.js
  create /mirage/serializers/application.js
Installed addon package.

This will create a mirage folder at the project root.

Lastly we'll install Faker which we are going to use to generate dummy data:

$ npm install --save-dev faker
For integration testing support read Mocking Integration Test Data with Ember CLI Mirage!

Step 1: Reading posts

We need an Ember model to work with, plus the list and single post pages:

$ ember g model post
$ ember g route posts
$ ember g route post --path 'posts/:post_id'

In Mirage, the starting point is the configuration file:

// mirage/config.js

export default function() {
  this.get('/posts');
  this.get('/posts/:id');
}

These are shorthands to declare endpoints for a list of posts (/posts) and a single post (/posts/:id).

By default, Mirage ships with a JSON API serializer so we can seamlessly start using it with Ember Data.

Seeding data with factories and scenarios

Let's define the attributes on the Ember Post model. We want it to have a title, a body and published date.

// app/models/post.js


import Model, { attr } from '@ember-data/model';

export default class PostModel extends Model {
  @attr title;
  @attr body;
  @attr('date') publishedAt;
}

Mirage automatically uses Ember Data models for the backend! Isn't that awesome?

Factories enable us to define “blueprints” or “templates” for our posts:

$ ember g mirage-factory post

Adding some attribute definitions (Faker will auto-generate gibberish for us):

// mirage/factories/post.js

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({

  title() {
    return faker.lorem.sentence();
  },

  body() {
    return faker.lorem.paragraph();
  },

  publishedAt() {
    return faker.date.past();
  }

});

And finally using the default scenario to seed the actual data:

// mirage/scenarios/default.js

export default function(server) {

  // generate 10 posts
  server.createList('post', 10);

}
One would typically use scenarios in development mode, as acceptance or integration tests would set up mock data via the server variable.

That's it! A fully functional backend for posts.

All that's left to do is add two simple routes to bring this blog to life!

Octane news & best practices, straight to your inbox?

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

List of posts page

// app/routes/posts.js

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

export default class PostsRoute extends Route {
  model() {
    return this.store.findAll('post');
  }
}
<!-- app/templates/posts.hbs -->

<h1>Posts</h1>

<ul>
  {{#each @model as |post|}}
    <li>
      <LinkTo @route="post" @model={{post.id}}>{{post.title}}</LinkTo>
    </li>
  {{/each}}
</ul>

Single post page

// app/routes/post.js

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

export default class PostRoute extends Route {
  model(params) {
    return this.store.findRecord('post', params.post_id);
  }
}
<!-- app/templates/post.hbs -->

<h1>{{@model.title}}</h1>

<p>
  <strong>Published: {{@model.publishedAt}}</strong>
</p>

<code>{{@model.body}}</code>

Running ember s and visiting /posts should hopefully show us the ten posts we told Mirage to generate for us:

diagram

Clicking on a title takes us to the post page:

diagram

Success 😄!

Step 2: Adding comments

Let's generate the Comment model and add a relationship with Post:

$ ember g model comment
// app/models/comment.js


import Model, { attr, belongsTo } from '@ember-data/model';

export default class CommentModel extends Model {
  @attr text;
  @attr('date') publishedAt;
  @belongsTo post;
}

Update the Post model, too:

// app/models/post.js


import Model, { attr, hasMany } from '@ember-data/model';

export default class PostModel extends Model {
  @attr title;
  @attr body;
  @attr('date') publishedAt;
  @hasMany comments;
}

Octane news & best practices, straight to your inbox?

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

Back to Mirage! Similarly to posts, let's create a comments factory.

$ ember g mirage-factory comment
// mirage/factories/comment.js

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({

  text() {
    return faker.lorem.sentence();
  },

  publishedAt() {
    return faker.date.past();
  }

});

But how are we going to query for comments? The answer is: JSON API!

Let's tell our post factory to create a bunch of comments with every single post:

// mirage/factories/post.js

import { Factory } from 'ember-cli-mirage';
import faker from 'faker';

export default Factory.extend({

  title() {
    return faker.lorem.sentence();
  },

  body() {
    return faker.lorem.paragraph();
  },

  publishedAt() {
    return faker.date.past();
  },

  afterCreate(post, server) {
    server.createList('comment', 3, { post });
  }

});

An alternative to factories is adding data is through fixtures.

Creating a fixture file is super easy:

$ ember g mirage-fixture comments

It will generate a plain JS file that we'll fill with dummy data:

// mirage/fixtures/comments.js

export default [
  { id: 1, text: "Amazing post number 1! Enjoyed it" },
  { id: 2, text: "Dude this post 1 is great, thanks!" }
];

Like factories, fixtures have to be explicitly loaded in our scenario. And yes, we can mix and match:

// mirage/scenarios/default.js

export default function(server) {

  // generate 10 posts
  server.createList('post', 10);

  server.loadFixtures('comments');
  server.create('comment', { postId: 5 });

}

Any arguments passed to the factory will override whatever it had generated for those particular attributes.

Ready to see comments in the blog post? Let's tell Ember Data to include comments any time we request a post:

// app/routes/post.js

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

export default class PostRoute extends Route {
  model(params) {
    return this.store.findRecord('post', params.post_id, { include: 'comments' });
  }
}

And display them:

<!-- app/templates/post.hbs -->

<h1>{{@model.title}}</h1>

<p>
  <strong>Published: {{@model.publishedAt}}</strong>
</p>

<code>{{@model.body}}</code>

<h4>Comments</h4>

<ul>
  {{#each @model.comments as |comment|}}
    <li>
      {{comment.text}}<br>
      <small> commented on {{comment.publishedAt}}</small>
    </li>
  {{/each}}
</ul>

Results in:

diagram

Step 3: Adding a comment box

Lastly, we are going to allow visitors to post comments. Simply add a shorthand to POST comments!

// mirage/config.js

export default function() {
  this.get('/posts');
  this.get('/posts/:id');
  this.post('/comments');
}

In our UI we'll add a textarea:

<!-- app/templates/post.hbs -->

<h1>{{@model.title}}</h1>

<p>
  <strong>Published: {{@model.publishedAt}}</strong>
</p>

<code>{{@model.body}}</code>

<h4>Comments</h4>

<Textarea @value={{this.commentDraft}} style="width: 400px; height: 50px" /><br>
<button {{on "click" (action this.addComment)}}>Add comment</button>

<ul>
  {{#each @model.comments as |comment|}}
    <li>
      {{comment.text}}<br>
      <small> commented on {{comment.publishedAt}}</small>
    </li>
  {{/each}}
</ul>

We just introduced this.commentDraft and this.addComment and since we are working in a template – we need to define these in its Javascript counterpart: the controller.

$ ember g controller post

So this.commentDraft will be a tracked property, a comment draft temporary value (it backs the value in the textarea).

And the submit action, which creates an Ember Data record and saves it. Pretty self-explanatory!

// app/controllers/post.js

import Controller from '@ember/controller';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class PostController extends Controller {

  @tracked commentDraft;

  @action
  addComment() {
    const comment = this.store.createRecord('comment', {
      text: this.commentDraft,
      publishedAt: new Date(),
      post: this.model
    });
    comment.save();
    this.commentDraft = "";
  }

}

Trying it out in the browser…

diagram

It works!

In Getting Started with Ember Octane we use a simplified version of a blog in Mirage, too!

(Exercise for the reader: Can you add comments to that app?)

How did you enjoy Mirage? Let me know in the comments! A big thanks to its creator @samselikoff.

Code available at: https://github.com/frank06/ember-mirage-blog

Enjoyed this article? Join Snacks!

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