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
# pnpm
pnpm add --save-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
, andMagazine
- All four Concrete types are in a GraphQL Union called
Media
Movie
andTV
implement aMovingPicture
interfaceBook
andMagazine
implement aWrittenMedia
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));
{ "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));
{ "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));
{ "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));
{ "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));
{ "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));
{ "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));
{ "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).