Skip to main content

Mirage JS

Mirage JS is a great tool that makes mocking resources backed by RESTful APIs easier. One of the main benefits of Mirage JS is that it provides a full in-memory database and ORM. This allows for mocked queries to be backed by stateful data, much like GraphQL Paper.

Note: If starting a new project it's recommended to use GraphQL Paper since it is based on the GraphQL Schema and is GraphQL-first in mocking data, along with graphql-mocks. If a project is already using Mirage JS then this guide will help adopt it for use with GraphQL using graphql-mocks and its tools.

This library provides a few ways that to extend GraphQL with Mirage JS including "Auto Resolvers" or by using Mirage JS within resolver functions, or a combination of both.

Installation

Install Mirage JS and the complementary @graphql-mocks/mirage package

# npm
npm install --save-dev miragejs @graphql-mocks/mirage

# yarn
yarn add --dev miragejs @graphql-mocks/mirage

Mirage JS Auto Resolvers Middleware

The mirageMiddleware will fill the Resolver Map with Auto Resolvers where resolvers do not already exist, unless replace option is provided. To control where resolvers are applied, specify the highlight option. The Middleware simply applies two types of resolvers to the Resolver Map: A Type Resolver for Abstract Types (Unions and Interfaces) and a Field Resolver for fields.

import { GraphQLHandler } from 'graphql-mocks';
import { mirageMiddleware } from '@graphql-mocks/mirage';

const handler = new GraphQLHandler({
middlewares: [mirageMiddleware()],
dependencies: {
mirageServer,
graphqlSchema,
},
});
  • mirageServer is a required dependency for this middleware.

Additional options on the mirageMiddleware include:

mirageMiddleware({
highlight: HighlightableOption,
replace: boolean,
});

How Mirage JS & Auto Resolving works

Mirage JS can be setup where:

  • Models and Relationships map to GraphQL types
  • Model attributes map to fields on GraphQL types

For example:

type Person {
name: String
family: [Person!]!
}
import { Model, hasMany } from 'miragejs';

Model.create({
family: hasMany('person'),
});

Associations between models reflect the relationships between GraphQL types. Relationships will be automatically resolved based on the matching naming between Mirage JS models and GraphQL types. This provides the basis for the auto resolving a GraphQL query. Auto Resolvers are applied to a Resolver map via the mirageMiddlware or can be imported individually if required.

Interface and Union Types

GraphQL Union and Interface are Abstract Types that represent concrete types. The mirageMiddleware Type Resolver provides two different strategies for resolving and modeling Abstract Types in Mirage. Both have their pros/cons and the best fit will depend on the use case. The accompanying examples are a bit verbose but demonstrate the extent of setting up these use cases. Both are setup with the same GraphQL Schema, query and return the same result. The __typename has been queried also to show the resolved discrete type.

The GraphQL Schema for these examples is:

export default `
schema {
query: Query
}

type Query {
person: Person!
}

type Person {
favoriteMedium: [Media]!
}

union Media = Movie | TV | Book | Magazine

interface MovingPicture {
title: String!
durationInMinutes: Int!
}

interface WrittenMedia {
title: String!
pageCount: String!
}

type Movie implements MovingPicture {
title: String!
durationInMinutes: Int!
director: String!
}

type TV implements MovingPicture {
title: String!
episode: String!
durationInMinutes: Int!
network: String!
}

type Book implements WrittenMedia {
title: String!
author: String!
pageCount: String!
}

type Magazine implements WrittenMedia {
title: String!
issue: String!
pageCount: String!
}
`;

This GraphQL schema has the following:

  • Four GraphQL Concrete Types: Movie, TV, Book, and Magazine
  • All four Concrete types are in a GraphQL Union called Media
  • Movie and TV implement a MovingPicture interface
  • Book and Magazine implement a WrittenMedia interface

One Model per Abstract Type

In this case a Mirage model (Media) is setup for the Abstract type itself, and instances specify their concrete type by the __typename attribute on the model, like __typename: 'Movie'. This option is easier and faster to setup but can become harder to manage and requires remembering to specify the __typename model attribute on each instance created.

import { GraphQLHandler } from "graphql-mocks";
import { mirageMiddleware } from "@graphql-mocks/mirage";
import { createServer, Model, hasMany } from "miragejs";
import { extractDependencies } from "graphql-mocks/resolver";
import graphqlSchema from "./abstract-type-schema.source";

const mirageServer = createServer({
models: {
Person: Model.extend({
favoriteMedium: hasMany("media"),
}),

// using a single model to represent _all_ the concrete types
Media: Model.extend(),
},
});

// All models created are for "media", but have their
// concrete type specified via __typename

const movie = mirageServer.schema.create("media", {
title: "The Darjeeling Limited",
durationInMinutes: 104,
director: "Wes Anderson",
__typename: "Movie",
});

const tvShow = mirageServer.schema.create("media", {
title: "Malcolm in the Middle",
episode: "Rollerskates",
network: "Fox",
durationInMinutes: 24,
__typename: "TV",
});

const book = mirageServer.schema.create("media", {
title: "The Hobbit, or There and Back Again",
author: "J.R.R. Tolkien",
pageCount: 310,
__typename: "Book",
});

const magazine = mirageServer.schema.create("media", {
title: "Lighthouse Digest",
issue: "May/June 2020",
pageCount: 42,
__typename: "Magazine",
});

mirageServer.schema.create("person", {
favoriteMedium: [movie, tvShow, book, magazine],
});

const graphqlHandler = new GraphQLHandler({
resolverMap: {
Query: {
person(_parent, _args, context) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);
return mirageServer.schema.people.first();
},
},
},

middlewares: [mirageMiddleware()],
dependencies: {
graphqlSchema,
mirageServer,
},
});

const query = graphqlHandler.query(`
{
person {
favoriteMedium {
__typename

... on MovingPicture {
title
durationInMinutes
}

... on Movie {
director
}

... on TV {
episode
network
}

... on WrittenMedia {
title
pageCount
}

... on Book {
author
}

... on Magazine {
issue
}
}
}
}
`);
query.then((result) => console.log(result));
Result:
{
  "data": {
    "person": {
      "favoriteMedium": [
        {
          "title": "The Darjeeling Limited",
          "director": "Wes Anderson",
          "durationInMinutes": 104,
          "__typename": "Movie"
        },
        {
          "title": "Malcolm in the Middle",
          "durationInMinutes": 24,
          "episode": "Rollerskates",
          "network": "Fox",
          "__typename": "TV"
        },
        {
          "title": "The Hobbit, or There and Back Again",
          "author": "J.R.R. Tolkien",
          "pageCount": "310",
          "__typename": "Book"
        },
        {
          "title": "Lighthouse Digest",
          "issue": "May/June 2020",
          "pageCount": "42",
          "__typename": "Magazine"
        }
      ]
    }
  }
}

One Model per Concrete Type

This option allows for each discrete type to be represented by its own Mirage Model definition. A relationship attribute that can hold an Abstract type should specify the { polymorphic: true } option on the relationship definition. This option sets up for distinct definitions but can also be more verbose.

import { GraphQLHandler } from "graphql-mocks";
import { mirageMiddleware } from "@graphql-mocks/mirage";
import { createServer, Model, hasMany } from "miragejs";
import { extractDependencies } from "graphql-mocks/resolver";
import graphqlSchema from "./abstract-type-schema.source";

const mirageServer = createServer({
models: {
Person: Model.extend({
// represent the abstract type with a polymorphic relationship
favoriteMedium: hasMany({ polymorphic: true }),
}),

// model definition exists for each discrete type
Movie: Model.extend(),
TV: Model.extend(),
Book: Model.extend(),
Magazine: Model.extend(),
},
});

const movie = mirageServer.schema.create("movie", {
title: "The Darjeeling Limited",
durationInMinutes: 104,
director: "Wes Anderson",
});

const tv = mirageServer.schema.create("tv", {
title: "Malcolm in the Middle",
episode: "Rollerskates",
network: "Fox",
durationInMinutes: 24,
});

const book = mirageServer.schema.create("book", {
title: "The Hobbit, or There and Back Again",
author: "J.R.R. Tolkien",
pageCount: 310,
});

const magazine = mirageServer.schema.create("magazine", {
title: "Lighthouse Digest",
issue: "May/June 2020",
pageCount: 42,
});

mirageServer.schema.create("person", {
favoriteMedium: [movie, tv, book, magazine],
});

const graphqlHandler = new GraphQLHandler({
resolverMap: {
Query: {
person(_parent, _args, context) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);
return mirageServer.schema.people.first();
},
},
},

middlewares: [mirageMiddleware()],
dependencies: {
graphqlSchema,
mirageServer,
},
});

const query = graphqlHandler.query(`
{
person {
favoriteMedium {
__typename

... on MovingPicture {
title
durationInMinutes
}

... on Movie {
director
}

... on TV {
episode
network
}

... on WrittenMedia {
title
pageCount
}

... on Book {
author
}

... on Magazine {
issue
}
}
}
}
`);
query.then((result) => console.log(result));
Result:
{
  "data": {
    "person": {
      "favoriteMedium": [
        {
          "title": "The Darjeeling Limited",
          "director": "Wes Anderson",
          "durationInMinutes": 104,
          "__typename": "Movie"
        },
        {
          "title": "Malcolm in the Middle",
          "durationInMinutes": 24,
          "episode": "Rollerskates",
          "network": "Fox",
          "__typename": "TV"
        },
        {
          "title": "The Hobbit, or There and Back Again",
          "author": "J.R.R. Tolkien",
          "pageCount": "310",
          "__typename": "Book"
        },
        {
          "title": "Lighthouse Digest",
          "issue": "May/June 2020",
          "pageCount": "42",
          "__typename": "Magazine"
        }
      ]
    }
  }
}

Mock the GraphQL Endpoint using Mirage JS Route Handlers

A GraphQL handler handles the mocked responses for GraphQL queries and mutations. However, GraphQL is agnostic to the network transport layer. Typically, GraphQL clients do use HTTP and luckily Mirage JS comes with out-of-the-box XHR interception and route handlers to mock this as well. GraphQL API Servers operate on a single endpoint for a query so only one route handler is needed. Migrating to other mocked networking methods later is easy as well.

Use createRouteHandler to get setup with a mocked GraphQL endpoint. Specify the same options as the GraphQLHandler constructor or specify a GraphQLHandler instance. This example sets up a GraphQLHandler on the graphql route.

import { createServer } from "miragejs";
import { createRouteHandler, mirageMiddleware } from "@graphql-mocks/mirage";

createServer({
routes() {
// capture mirageServer dependency
const mirageServer = this;

// create a route handler for POSTs to `/graphql`
// using `createRouteHandler`
this.post(
"graphql",
createRouteHandler({
middlewares: [mirageMiddleware()],
dependencies: {
mirageServer,
graphqlSchema,
},
})
);
},
});

The MirageServer instance can be referenced by this within the routes() function and must be passed in as the mirageServer dependency. See the Mirage JS route handlers documentation for more information about mocking HTTP endpoints with route handlers.

Note: The rest of the examples skip this part and focus on graphql-mocks and Mirage JS configuration and examples.

Relay Pagination

Use the relayWrapper for quick relay pagination. It must be after the Mirage JS Middleware. The @graphql-mocks/mirage package provides a mirageCursorForNode function to be used for the required cursorForNode argument.

Check out the Relay Wrapper documentation for more details.

import { GraphQLHandler } from 'graphql-mocks';
import { mirageCursorForNode } from '@graphql-mocks/mirage';

const handler = new GraphQLHandler({
middlewares: [
mirageMiddleware(),
embed({
wrappers: [
relayWrapper({ cursorForNode: mirageCursorForNode })
]
})
],
dependencies: {
mirageServer,
graphqlSchema,
},
});

Examples

Basic Query

This example shows the result of querying with Auto Resolvers against Mirage Models with relationships (between a Wizard and their spells). It uses the mirageMiddlware middlware, sets up dependencies and runs a query. The mutations will persist as part of Mirage JS's in-memory database for future mutations and queries.

import { GraphQLHandler } from "graphql-mocks";
import { mirageMiddleware } from "@graphql-mocks/mirage";
import { createServer, Model, hasMany } from "miragejs";

// Define GraphQL Schema
const graphqlSchema = `
schema {
query: Query
}

type Query {
movies: [Movie!]!
}

type Movie {
title: String!
actors: [Actor!]!
}

type Actor {
name: String!
}
`;

// Create the mirage server and schema
const mirageServer = createServer({
models: {
Actor: Model,
Movie: Model.extend({
actors: hasMany(),
}),
},
});

// Create model instances
const meryl = mirageServer.schema.create("actor", { name: "Meryl Streep" });
const bill = mirageServer.schema.create("actor", { name: "Bill Murray" });
const anjelica = mirageServer.schema.create("actor", {
name: "Anjelica Huston",
});

mirageServer.schema.create("movie", {
title: "Fantastic Mr. Fox",
actors: [meryl, bill],
});
mirageServer.schema.create("movie", {
title: "The Life Aquatic with Steve Zissou",
actors: [bill, anjelica],
});

const graphqlHandler = new GraphQLHandler({
middlewares: [mirageMiddleware()],
dependencies: {
graphqlSchema,
mirageServer,
},
});

const query = graphqlHandler.query(`
{
movies {
title
actors {
name
}
}
}
`);
query.then((result) => console.log(result));
Result:
{
  "data": {
    "movies": [
      {
        "title": "Fantastic Mr. Fox",
        "actors": [
          {
            "name": "Meryl Streep"
          },
          {
            "name": "Bill Murray"
          }
        ]
      },
      {
        "title": "The Life Aquatic with Steve Zissou",
        "actors": [
          {
            "name": "Bill Murray"
          },
          {
            "name": "Anjelica Huston"
          }
        ]
      }
    ]
  }
}

Mutations (Create, Update, Delete)

GraphQL Mutations can be done with static resolvers and a reference to the mirageServer dependency using the extractDependencies function.

resolverFunction: function(root, args, context, info) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);
}

Create Example with Input Variables

This example creates a new instance of a Wizard model on the Mirage JS using a GraphQL Input Type.

import { GraphQLHandler } from "graphql-mocks";
import { createServer, Model } from "miragejs";
import { extractDependencies } from "graphql-mocks/resolver";

const mirageServer = createServer({
models: {
Movie: Model,
},
});

const graphqlSchema = `
schema {
query: Query
mutation: Mutation
}

type Query {
Movie: [Movie!]!
}

type Mutation {
# Create mutation
addMovie(input: AddMovieInput): Movie!
}

type Movie {
id: ID!
title: String!
style: MovieStyle!
}

input AddMovieInput {
title: String!
style: MovieStyle!
}

enum MovieStyle {
LiveAction
StopMotion
Animated
}
`;

// Represents the resolverMap with our static Resolver Function
// using `extractDependencies` to handle the input args and
// return the added Movie
const resolverMap = {
Mutation: {
addMovie(_root, args, context, _info) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);

const addedMovie = mirageServer.schema.movies.create({
title: args.input.title,
style: args.input.style,
});

return addedMovie;
},
},
};

const handler = new GraphQLHandler({
resolverMap,
dependencies: {
graphqlSchema,
mirageServer,
},
});

const mutation = handler.query(
`
mutation($movie: AddMovieInput) {
addMovie(input: $movie) {
id
title
style
}
}
`,

// Pass external variables for the mutation
{
movie: {
title: "Isle of Dogs",
style: "StopMotion",
},
}
);
mutation.then((result) => console.log(result));
Result:
{
  "data": {
    "addMovie": {
      "id": "1",
      "title": "Isle of Dogs",
      "style": "StopMotion"
    }
  }
}

Update Example

In this example Voldemort, Tom Riddle, has mistakenly been put into the wrong Hogwarts house. Using the updateHouse mutation will take his Model ID, the correct House, and return the updated data. The resolverMap has a updateHouse Resolver Function that will handle this mutation and update the within Mirage JS.

import { GraphQLHandler } from "graphql-mocks";
import { createServer, Model } from "miragejs";
import { extractDependencies } from "graphql-mocks/resolver";

const mirageServer = createServer({
models: {
movie: Model,
},
});

// Create the movie "The Royal Tenenbaums" in Mirage JS
// Whoops! It's been assigned the wrong year but we can
// fix this via a GraphQL Mutation
const royalTenenbaums = mirageServer.schema.create("movie", {
name: "The Royal Tenenbaums",
year: "2020",
});

const graphqlSchema = `
schema {
query: Query
mutation: Mutation
}

type Query {
movies: [Movie!]!
}

type Mutation {
# Update
updateYear(movieId: ID!, year: String!): Movie!
}

type Movie {
id: ID!
name: String!
year: String!
}
`;

const resolverMap = {
Mutation: {
updateYear(_root, args, context, _info) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);

// lookup and update the year on the movie with args
const movie = mirageServer.schema.movies.find(args.movieId);
movie.year = args.year;
movie.save();

return movie;
},
},
};

const handler = new GraphQLHandler({
resolverMap,
dependencies: {
graphqlSchema,
mirageServer,
},
});

const mutation = handler.query(
`
mutation($movieId: ID!, $year: String!) {
updateYear(movieId: $movieId, year: $year) {
id
name
year
}
}
`,

// Pass external variables for the mutation
{
movieId: royalTenenbaums.id, // corresponds with the model we created above
year: "2001",
}
);
mutation.then((result) => console.log(result));
Result:
{
  "data": {
    "updateYear": {
      "id": "1",
      "name": "The Royal Tenenbaums",
      "year": "2001"
    }
  }
}

Delete Example

Removing Voldemort's entry in the Mirage JS database can be done through a removeWizard mutation. The resolverMap has a removeWizard Resolver Function that will handle this mutation and update the within Mirage JS.

import { GraphQLHandler } from "graphql-mocks";
import { createServer, Model } from "miragejs";
import { extractDependencies } from "graphql-mocks/resolver";

const mirageServer = createServer({
models: {
movie: Model,
},
});

const grandBudapestHotel = mirageServer.schema.create("movie", {
title: "The Grand Budapest Hotel",
});

const hamilton = mirageServer.schema.create("movie", {
title: "Hamilton",
});

const graphqlSchema = `
schema {
query: Query
mutation: Mutation
}

type Query {
movies: [Movie!]!
}

type Mutation {
# Remove
removeMovie(movieId: ID!): Movie!
}

type Movie {
id: ID!
title: String!
}
`;

const resolverMap = {
Mutation: {
removeMovie(_root, args, context, _info) {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);

const movie = mirageServer.schema.movies.find(args.movieId);
movie.destroy();

return movie;
},
},
};

const handler = new GraphQLHandler({
resolverMap,
dependencies: {
graphqlSchema,
mirageServer,
},
});

const mutation = handler.query(
`
mutation($movieId: ID!) {
removeMovie(movieId: $movieId) {
id
title
}
}
`,

// Pass external variables for the mutation
{
movieId: hamilton.id,
}
);
mutation.then((result) => console.log(result));
Result:
{
  "data": {
    "removeMovie": {
      "id": "2",
      "title": "Hamilton"
    }
  }
}

Static Resolver Functions

Mirage JS can be used directly in static Resolver Functions in a Resolver Map by using the extractDependencies utility. This technique can be with Mutations, and Query Resolver Functions to bypass Auto Resolving while still having access to Mirage. This is usually done when fine-grained control is needed.

import { GraphQLHandler } from "graphql-mocks";
import { extractDependencies } from "graphql-mocks/resolver";
import { mirageMiddleware } from "@graphql-mocks/mirage";
import { createServer, Model } from "miragejs";

const graphqlSchema = `
schema {
query: Query
}

type Query {
movies: [Movie!]!
}

type Movie {
name: String!
}
`;

const mirageServer = createServer({
models: {
Movie: Model,
},
});

mirageServer.schema.create("movie", {
name: "Moonrise Kingdom",
});

mirageServer.schema.create("movie", {
name: "The Darjeeling Limited",
});

mirageServer.schema.create("movie", {
name: "Bottle Rocket",
});

const resolverMap = {
Query: {
movies: (_parent, _args, context, _info) => {
const { mirageServer } = extractDependencies(context, ["mirageServer"]);
return mirageServer.schema.movies.all().models;
},
},
};

const handler = new GraphQLHandler({
resolverMap,

// Note: the `mirageMiddleware` is only required for handling downstream
// mirage relationships from the returned models. Non-relationship
// attributes on the model will "just work"
middlewares: [mirageMiddleware()],

dependencies: {
graphqlSchema,
mirageServer,
},
});

const query = handler.query(`
{
movies {
name
}
}
`);
query.then((result) => console.log(result));
Result:
{
  "data": {
    "movies": [
      {
        "name": "Moonrise Kingdom"
      },
      {
        "name": "The Darjeeling Limited"
      },
      {
        "name": "Bottle Rocket"
      }
    ]
  }
}

Comparison with miragejs/graphql

Mirage JS has a GraphQL solution, miragejs/graphql, that leverages mirage & graphql automatic mocking and sets up models on the mirage schema automatically. graphql-mocks with @graphql-mocks/mirage do a few things differently than miragejs/graphql.

  • This library focuses on providing a flexible GraphQL-first mocking experience using Middlewares and Wrappers, and mainly uses Mirage JS as a stateful store. While Mirage JS focuses on mocking REST and uses @miragejs/graphql as an extension to provide GraphQL resolving.
  • This library also does not apply automatic filtering like @miragejs/graphql as this tends to be highly specific to the individual GraphQL API. The same result, however, can be achieved by using a Resolver Wrapper, see Automatic Filtering with Wrappers for examples.
  • This library currently does not setup the Mirage JS Schema with Models and relationships based on the GraphQL Schema but aims at adding this as a configuration option in the future (PRs are welcome).