To define routes for your server, use the get
, post
, put
(or patch
) and del
methods. Here’s an example:
// mirage/config.js
this.get('/authors', () => {
return ['Link', 'Zelda', 'Epona'];
});
Now when your Ember app makes a GET request to /authors
, it will receive this data.
Each verb method has the same signature. The first argument is the path (URL) and the second is the actual function handler that returns the response:
this.verb( path, handler )
Here are some more examples:
this.get('/users/current', () => {
return {
name: 'Zelda',
email: 'z@hyrule.org'
};
});
this.get('/events', () => {
let events = [];
for (let i = 1; i < 1000; i++) {
events.push({id: i, value: Math.random()});
};
return events;
});
In the examples above, we wrote the response data directly in the route. Instead of doing this, Mirage provides a simple in-memory database you can use to make your routes more versatile.
In previous versions of Mirage, you interacted with the database directly in your route handlers. Models were introduced as a wrapper around the database to provide better means for dealing with associations and formatting responses, which we’ll get to shortly. For now, it’s enough to know that models are the primary way you interact with Mirage’s database.
So, let’s first define an author
model. We can do this from the command line:
ember g mirage-model author
This gives us the file
// mirage/models/author.js
import { Model } from 'ember-cli-mirage';
export default Model;
which sets up our database (at run-time) with an authors
table. Now we can rewrite our route handler to use the model.
A schema
object is injected into each route handler as the first parameter, which is how we access our models. Let’s use it to make this route dynamic:
// mirage/config.js
this.get('/authors', (schema) => {
return schema.authors.all();
});
Now, Mirage will respond to this route with all the author
models in its database.
You can also create, update and delete models from your route handlers. A request
object is injected as the second parameter, which contains any data that was sent along with the request.
Let’s say your Ember app creates an author by sending a POST request with the following payload:
{
author: {
name: 'Link',
age: 123
}
}
We can use this data to create a new author
model:
this.post('/authors', (schema, request) => {
const attrs = JSON.parse(request.requestBody).author;
return schema.authors.create(attrs);
});
This handler creates a new author, inserts it into the database (which assigns it an id
), and responds with that record.
As long as all your Mirage routes read from and write to the database, user interactions will persist during a single session. This lets users interact with your app as if it were wired up to a real server.
The request object that’s injected into your route handlers contains any dynamic route segments and query params.
To define a route that has a dynamic segment, use colon syntax (:segment
) in your path. The dynamic piece will be available via request.params.[segment]
:
this.get('/authors/:id', (schema, request) => {
var id = request.params.id;
return schema.authors.find(id);
})
Query params from the request can also be accessed via request.queryParams.[param]
.
Mirage uses Pretender.js to intercept HTTP requests, so check out its docs to see the full API for the request object.
APIs have become more standardized, so Mirage has the concept of shorthands to deal with common scenarios. These shorthands can replace many of your custom route handlers, dramatically simplifying your server definition.
For example, the route handler we wrote above to handle a GET request to /authors
this.get('/authors', (schema, request) => {
return schema.authors.all();
});
is pretty standard: it responds with a named collection. The shorthand form of this is
this.get('/authors');
which infers the collection name from the last part of the URL. Returning a single author by id is just as easy:
this.get('/authors/:id');
Similarly, we wrote a route handler by hand to deal with creating an author
this.post('/authors', (schema, request) => {
const attrs = JSON.parse(request.requestBody).author;
return schema.authors.create(attrs);
});
which is also pretty standard: it creates a new model using the attributes from the request payload. The equivalent shorthand is
this.post('/authors');
View the full reference to see all available shorthands.
When you return a model or a collection from a route handler, Mirage serializes it into a JSON payload, and then responds to your Ember app with that payload. It uses an object called a Serializer to do this, which you can customize. Having a single object that’s responsible for this formatting logic helps keep your route handlers simple. In particular, a bit of customization in the serializer layer often lets you use shorthands when you otherwise wouldn’t be able to.
Mirage ships with three named serializers, JsonApiSerializer (used to implement JSON:API), ActiveModelSerializer (used to simulate Rails servers using ActiveModel::Serializers) and RestSerializer (use to match Ember Data’s RestSerializer expected response format). You should use these if your app’s backend will be built to conform to either standard.
Additionally, there’s a basic Serializer class that you can use and customize. By default, it takes all the attributes of your model, and returns them under a root key of the model type. Suppose you had the following author in your database:
{
id: 1,
firstName: 'Keyser',
lastName: 'Soze'
age: 145
}
and you had this route
this.get('/authors/:id');
The response would look like this:
GET /authors/1
{
author: {
id: 1,
firstName: 'Keyser',
lastName: 'Soze',
age: 145
}
}
Remember, your Mirage server should mimic your backend server. Let’s say your backend server returns dasherized object keys instead of camel cased. You can customize the response by extending the base Serializer and overwriting keyForAttribute
:
// mirage/serializers/application.js
import { Serializer } from 'ember-cli-mirage';
import Ember from 'ember';
const { dasherize } = Ember.String;
export default Serializer.extend({
keyForAttribute(key) {
return dasherize(key);
}
});
Now, without having to edit your route handler, the response would look like this:
GET /authors/1
{
author: {
id: 1,
'first-name': 'Keyser',
'last-name': 'Soze',
age: 145
}
}
Just like in Ember, the serializers/application.js
file will apply to all your responses. You can also create per-model serializers, for example to include only some attributes:
// mirage/serializers/author.js
import ApplicationSerializer from './application';
export default ApplicationSerializer.extend({
attributes: ['firstName']
});
View the full Serializer API to learn more about customizing your responses.
You can define associations between your models, which makes dealing with related data easier. For example, let’s say an author has many posts, and when you delete an author your server also deletes all the related posts.
We can define the relationship on our author model:
// mirage/models/author.js
import { Model, hasMany } from 'ember-cli-mirage';
export default Model.extend({
posts: hasMany()
});
hasMany
tells Mirage to manage a postIds: []
array on our author model. Now, our route handler for deleting an author looks like this:
this.del('/authors/:id', (schema, request) => {
let author = schema.authors.find(request.params.id);
author.posts.delete();
author.delete();
});
A response with DELETE’s default status code of 200 would then be returned.
Associations are also useful in combination with serializers. With the relationship defined above on our author
model, we can use a shorthand and a custom serializer to sideload an author’s related posts:
// mirage/config.js
this.get('/authors/:id');
// mirage/serializers/author.js
import { Serializer } from 'ember-cli-mirage';
export default Serializer.extend({
include: ['posts']
});
Now a request for an author responds with the following:
GET /authors/1
{
author: {
id: 1,
name: 'Link'
},
posts: [
{id: 1, authorId: 1, title: "The Beauty of Hyrule"},
{id: 2, authorId: 1, title: "Really, I'm an Elf!"},
{id: 3, authorId: 1, title: "Ganondorf is my Father"}
]
}
Mirage’s database uses camelCase for all model attributes, including foreign keys (e.g. authorId
in the example above), but you can customize the format that gets sent in the response by overwriting your serializer’s keyForRelatedCollection
. See the serializer guide for more details.
By default, Mirage sets the HTTP code of a response based on the verb being used:
get
, put
and del
are 200post
is 201Additionally, a header for Content-Type
is set to application/json
.
You can customize both the response code and headers by returning a Response
in your route handler:
// app/mirage/config.js
import { Response } from 'ember-cli-mirage';
export default function() {
this.post('/authors', function(schema, request) {
let attrs = JSON.parse(request.requestBody).author;
if (attrs.name) {
return schema.authors.create(attrs);
} else {
return new Response(400, {some: 'header'}, {errors: ['name cannot be blank']});
}
});
}
Route handlers for paths without a domain (e.g. this.get('/authors')
) work for requests that target the current domain. To simulate other-origin requests, specify the fully qualified URL for your route handler:
this.get('https://api.github.com/users/samselikoff/events', () => {
return [];
});
That’s the essentials of defining your routes. Next, you’ll learn how to seed your database with some starting data, both in development and within tests.