Overview
Mirage lets you simulate API responses by writing route handlers.
The simplest example of a route handler is a function that returns an object:
import { createServer } from "miragejs"
createServer({
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
Now whenever your app makes a GET request to /api/movies
, Mirage will respond with this data.
If your API is on a different host or port than your app, set urlPrefix
:
routes() {
this.urlPrefix = 'http://localhost:3000';
You can get pretty far using static route handlers like this, and they're a good way to get comfortable working with Mirage. All the HTTP verbs work, there's a timing
option you can use to simulate a slow server, and you can even return a custom Response
to see how your app behaves when it receives an error from your API.
import { createServer, Response } from "miragejs"
createServer({
routes() {
this.namespace = "api"
// Responding to a POST request
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
attrs.id = Math.floor(Math.random() * 100)
return { movie: attrs }
})
// Using the `timing` option to slow down the response
this.get(
"/movies",
() => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
},
{ timing: 4000 }
)
// Using the `Response` class to return a 500
this.delete("/movies/1", () => {
let headers = {}
let data = { errors: ["Server did not respond"] }
return new Response(500, headers, data)
})
},
})
Dynamic route handlers
Static route handlers work, and they're a common way to simulate HTTP responses – but hard-coded responses like the ones above have a few problems:
They're inflexible. What if you want to change the data that a route responds with for a single test? You now have to rewrite the entire handler from scratch.
They contain formatting logic. Logic that's concerned with the shape of your JSON payload (e.g. the
movies: []
root key in our payload above) is now duplicated across all your route handlers.They're too basic. Inevitably, when your Mirage server needs to deal with more complex things like relationships, these simple ad hoc responses start to break down.
Mirage has a Data layer to help you write a more powerful server implementation. Let's see how it works by replacing our basic stub data above.
First, we'll tell Mirage that we have a dynamic Movie
model:
import { createServer, Model } from "miragejs"
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", () => {
return {
movies: [
{ id: 1, name: "Inception", year: 2010 },
{ id: 2, name: "Interstellar", year: 2014 },
{ id: 3, name: "Dunkirk", year: 2017 },
],
}
})
},
})
Models let our route handlers take advantage of Mirage's in-memory database. The database makes our route handlers dynamic, so we can change the data that's returned without having to rewrite the handler.
Let's update our route handler to be dynamic:
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
The schema
argument is how we access our new Movie
model. This route will now respond with all the authors in Mirage's database at the time of the request. We can therefore change the data this route responds with by only changing what records are in Mirage's database.
The last step is to seed the database. Right now, if we sent a request to our new handler above, the response would look something like this:
// GET /api/movies
{
"movies": []
}
That's because Mirage's database is empty. We can use seeds to start out our database with some initial data:
createServer({
models: {
movie: Model,
},
routes() {
this.namespace = "api"
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
},
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
},
})
server.create
takes a model name and an attributes object, and inserts the new data into the database.
Now, when our JavaScript app makes a request to /api/movies
, our server responds with this:
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
Notice how Mirage's database automatically assigned an auto-incrementing ID for each record.
We've also eliminated all hard-coded data from our response, meaning that if our app modifies the data in Mirage's database over time, the response to this endpoint will change accordingly.
Hopefully you can see how the database, models and the Schema
API drastically simplify our server definition. Here's a set of five standard RESTful routes for our Movie
resource:
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
this.get("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id)
})
this.post("/movies", (schema, request) => {
let attrs = JSON.parse(request.requestBody)
return schema.movies.create(attrs)
})
this.patch("/movies/:id", (schema, request) => {
let newAttrs = JSON.parse(request.requestBody)
let id = request.params.id
let movie = schema.movies.find(id)
return movie.update(newAttrs)
})
this.delete("/movies/:id", (schema, request) => {
let id = request.params.id
return schema.movies.find(id).destroy()
})
With this Mirage definition in place, you can fully build out and test your frontend app, completing every dynamic feature and accounting for every state in which your server can exist. Once you're happy with your code, you'll be ready to deploy it against a production server that fulfills this same API contract as your Mirage definition.
Shorthands
Mirage has a concept of Shorthands to reduce the code needed for conventional API endpoints.
For example, the route handler
this.get("/movies", (schema, request) => {
return schema.movies.all()
})
can be written as
this.get("/movies")
There are also Shorthands for post
, patch
(or put
), and del
methods. Here's the full set of resourceful routes for our Movie
resource we defined above, written using Shorthands:
this.get("/movies")
this.get("/movies/:id")
this.post("/movies")
this.patch("/movies/:id")
this.del("/movies/:id")
Shorthands make writing your server definition concise, so use them whenever possible. When mocking a new route, you should always start with a Shorthand, and then drop down to an expanded function route handler when you need more control.
Factories
In the example above, we seeded Mirage's database using the server.create
API:
seeds(server) {
server.create("movie", { name: "Inception", year: 2010 })
server.create("movie", { name: "Interstellar", year: 2014 })
server.create("movie", { name: "Dunkirk", year: 2017 })
}
While it's nice to be able to pass in every attribute for each record, sometimes we just want a faster way to create new database records. That's where factories come in.
Factories are objects that make it easy to generate realistic-looking data for your Mirage server. Think of them as blueprints for your models.
We can create a Factory for our Movie
model like this:
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({}),
},
})
We can then define some properties on our Factory. They can be simple types like Booleans, Strings or Numbers, or functions that return dynamic data:
import { createServer, Model, Factory } from "miragejs"
createServer({
models: {
movie: Model,
},
factories: {
movie: Factory.extend({
title(i) {
return `Movie ${i}` // Movie 1, Movie 2, etc.
},
year() {
let min = 1950
let max = 2019
return Math.floor(Math.random() * (max - min + 1)) + min
},
rating: "PG-13",
}),
},
})
Now when we use the server.create
API, Mirage will use our Factory to help us generate new data. (It still respects attribute overrides we pass in.)
server.create("movie")
server.create("movie")
server.create("movie", { rating: "R" })
server.db.dump()
/*
Mirage's database now contains
{
movies: [
{
id: 1,
title: "Movie 1",
year: 1992,
rating: "PG-13",
},
{
id: 2,
title: "Movie 2",
year: 2008,
rating: "PG-13",
},
{
id: 3,
title: "Movie 3",
year: 1947,
rating: "R",
}
]
}
*/
There's also a server.createList
API to generate many records at once.
You can use both server.create
and server.createList
to invoke your Factories in your seeds
function:
import { createServer, Factory } from "miragejs"
createServer({
seeds(server) {
server.createList("movie", 10)
},
})
as well as in your testing environment. In a test environment, Mirage will load its routes but ignore its seeds, giving you the opportunity to set up the database in exactly the state needed for your test:
// app-test.js
import React from "react"
import { render, waitForElement } from "@testing-library/react"
import App from "./App"
import startMirage from "./start-mirage"
let server
beforeEach(() => {
server = startMirage({ environment: "test" })
})
afterEach(() => {
server.shutdown()
})
it("shows the list of movies", async () => {
server.createList("movie", 5)
const { getByTestId } = render(<App />)
await waitForElement(() => getByTestId("movie-list"))
expect(getByTestId("movie-item")).toHaveLength(5)
})
Factories give you a simple way to set up your Mirage server's initial data, both during development and on a per-test basis.
Relationships
Dealing with relationships is always tricky, and mocking endpoints that deal with relationships is no exception. Fortunately, Mirage ships with an ORM to help keep your route handlers clean.
Let's say your Movie
has many CastMembers
. You can declare this relationship in your model:
import { createServer, hasMany, belongsTo } from "miragejs"
createServer({
models: {
movie: Model.extend({
castMembers: hasMany(),
}),
castMember: Model.extend({
movie: belongsTo(),
}),
},
})
Now Mirage knows about the relationship between these two models, which can be useful when writing route handlers:
this.get("/movies/:id/cast-members", (schema, request) => {
let movie = schema.movies.find(request.params.id)
return movie.castMembers
})
and when creating graphs of related data:
it("shows the cast members for a movie", async () => {
const movie = server.create("movie", {
title: "Interstellar",
castMembers: [
server.create("cast-member", { name: "Matthew McConaughey" }),
server.create("cast-member", { name: "Anne Hathaway" }),
server.create("cast-member", { name: "Jessica Chastain" }),
],
})
const { getByTestId } = render(<App path={`/movies/${movie.id}`} />)
await waitForElement(() => getByTestId("cast-member-list"))
expect(getByTestId("cast-member")).toHaveLength(3)
})
Mirage uses foreign keys to keep track of these related models for you, so you don't have to worry about any messy bookkeeping details as your JavaScript app reads and writes new relationships to the database.
Serializers
Mirage is designed so that you can completely reproduce your production API server.
So far, we've seen that Mirage's default payloads are formatted like this:
// GET /api/movies
{
"movies": [
{ "id": 1, "name": "Inception", "year": 2010 },
{ "id": 2, "name": "Interstellar", "year": 2014 },
{ "id": 3, "name": "Dunkirk", "year": 2017 }
]
}
But of course, not every backend API matches this format.
For example, your API might be using the JSON:API spec and look more like this:
// GET /api/movies
{
"data": [
{
"id": 1,
"type": "movies",
"attributes": { "name": "Inception", "year": 2010 }
},
{
"id": 2,
"type": "movies",
"attributes": { "name": "Interstellar", "year": 2014 }
},
{
"id": 3,
"type": "movies",
"attributes": { "name": "Dunkirk", "year": 2017 }
}
]
}
This is why Mirage serializers exist. Serializers let you customize the formatting logic of your responses, without having to change your route handlers, models, relationships, or any other part of your Mirage setup.
Mirage ships with a few named serializers that match popular backend formats:
import { createServer, JSONAPISerializer } from "miragejs"
createServer({
serializers: {
application: JSONAPISerializer,
},
})
You can also extend from the base class and use its formatting hooks to write your own:
import { createServer, Serializer } from "miragejs"
createServer({
serializers: {
application: Serializer.extend({
keyForAttribute(attr) {
return dasherize(attr)
},
keyForRelationship(attr) {
return dasherize(attr)
},
}),
},
})
Mirage's serializer layer is aware of your relationships, which helps when mocking endpoints that are expected to sideload or embed related data.
For example, with the following configuration
createServer({
serializers: {
movie: Serializer.extend({
include: ["crewMembers"],
}),
},
routes() {
this.get("/movies/:id")
},
})
a GET to /movies/1
would automatically include related crew members:
// GET /movies/1
{
"movie": {
"id": 1,
"title": "Interstellar"
},
"crew-members": [
{
"id": 1,
"movie-id": 1,
"name": "Matthew McConaughey"
},
{
"id": 2,
"movie-id": 1,
"name": "Anne Hathaway"
},
{
"id": 3,
"movie-id": 1,
"name": "Jessica Chastain"
}
]
}
Mirage's named serializers do a lot of this kind of work for you, so you should use them as a starting point, and only add customizations that are specific to your API once you need them.
Passthrough
Mirage is a great tool to use even if you're working on an existing app, or if you don't want to mock your entire API. By default, Mirage will throw an error if your JavaScript app makes a request that doesn't have a corresponding route handler defined.
To avoid this, tell Mirage to let unhandled requests pass through:
createServer({
routes() {
// Allow unhandled requests on the current domain to pass through
this.passthrough()
},
})
Now you can develop as you normally would, for example against an existing API.
When it comes time to build a new feature, you don't have to wait for the API to be updated. Just define the new route that you need
createServer({
routes() {
// Mock this route and Mirage will intercept it
this.get("/movies")
// All other API requests on the current domain will still pass through
// e.g. GET /api/directors
this.passthrough()
// If your API requests go to an external domain, pass those through by
// specifying the fully qualified domain name
this.passthrough("http://api.acme.com/**")
},
})
and you can fully develop and test the feature. In this way you can build up your server definition piece by piece - adding some solid acceptance tests for each state of your server as you go.
That should be enough to get you started!
The next section of the docs covers each one of Mirage's main concepts in detail.