Describing GraphQL API

Learn how to describe GraphQL API with Mock Service Worker.

Prerequisites

GraphQL client

Although it’s not mandatory, we highly recommend using a GraphQL client to make requests to the (mocked) GraphQL API. Having a GraphQL client has no effect on this tutorial but instead results in a production-ready code which, in return, yields a much better developer experience and specification-compliance.

Here are some of the popular GraphQL clients to choose from:

You can, of course, make GraphQL requests using plain window.fetch or any other HTTP client. If you decide to do so, make sure you follow the GraphQL over HTTP specification. MSW takes that specification into account when distinguishing between GraphQL requests and unrelated HTTP requests.

Import

MSW provides a designated graphql namespace for describing GraphQL operations. We will use that namespace to describe what operations to intercept and how to respond to them.

Import the graphql namespace from the msw package:

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = []

Request handler

Next, we will create a Request handler. Methods on the graphql namespace allow us to create request handlers to intercept GraphQL operations of corresponding types, like graphql.query() for queries or graphql.mutation() for mutations.

graphql[operationType](operationName, resolver)

Request handlers are functions that allow you to Intercept requests and Mock responses.

In this tutorial, we will describe a basic GraphQL API that covers the following operations:

  • query ListPosts, to return all existing posts;
  • mutation CreatePost, to create a new post;
  • mutation DeletePost, to delete a post by ID.

Let’s start by creating a request handler for the ListPosts query.

Call graphql.query() to declare your first request handler

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = [
  graphql.query('ListPosts', ({ query }) => {
    console.log('Intercepted a "ListPosts" GraphQL query:', query)
  }),
]

Notice that we don’t specify the exact server endpoint when describing this operation. By default, MSW will intercept all matching GraphQL operations regardless of their destination. You can opt-in into the endpoint-based GraphQL interception by using the graphql.link() API. That can be handy if your application communicates with multiple GraphQL servers at the same time. In this tutorial, we will describe a single GraphQL server so we don’t need that API.

Following the same example, add request handlers for the remaining operations:

// src/mocks/handlers.js
import { graphql } from 'msw'
 
export const handlers = [
  graphql.query('ListPosts', ({ query }) => {
    console.log('Intercepted a "ListPosts" GraphQL query:', query)
  }),
  graphql.mutation('CreatePost', ({ query, variables }) => {
    console.log(
      'Intercepted a "CreatePost" GraphQL mutation:',
      query,
      variables
    )
  }),
  graphql.mutation('DeletePost', ({ query, variables }) => {
    console.log('Intercepted a "DeletePost" GraphQL mutation', query, variables)
  }),
]

Response resolver

Response resolver is the second argument to the request handler that decides how to handle the intercepted request. There are multiple things you can do with such a request: respond with a mock response, perform it as-is, perform a proxy request and augment the original response, etc. You can always learn more about response resolver on this pages:

In this tutorial, we will be responding to the intercepted requests with mock responses.

Mocking responses

Responding to GraphQL requests follows the same principle as responding to any other HTTP request: construct a valid Fetch API Response instance and return it from the response resolver.

GraphQL responses, however, have a number of differences:

  • GraphQL responses are always 200 OK, even in case of error responses;
  • GraphQL response bodies must have a fixed structure ({ data, errors, extensions });

In order for your GraphQL client to understand and process the mocked responses, they must be valid GraphQL responses. They must also comply with the response body shape expected by the particular GraphQL client (e.g. Apollo expects root-level types to contain the __typename string property). Consult the documentation of the GraphQL client you are using and observe its responses to make sure your request handlers comply with what the client expects.

Let’s declare a mock response for the ListPosts query containing a list of all posts.

Import the HttpResponse class from msw:

import { graphql, HttpResponse } from 'msw'

Learn about what HttpResponse is and why you should use it over the standard Response in here.

Next, we will describe the mock response body. When working with a GraphQL API, the shape of the response body must match the shape of the query. For example, let’s say the client performs the ListPosts query that looks like this:

query ListPosts {
  posts {
    id
    title
  }
}

This means that the client expects a response with the posts root-level property that represents an array of posts. Each array item (i.e. post) then contains properties id and title. Based on this query definition, we can construct a mock JSON response using HttpResponse.json() static method.

Return a mock response from the query ListPosts resolver:

import { graphql, HttpResponse } from 'msw'
 
// Represent the list of all posts in a Map.
const allPosts = new Map([
  [
    'e82f332c-a4e7-4463-b440-59bc91792634',
    {
      id: 'e82f332c-a4e7-4463-b440-59bc91792634',
      title: 'Introducing a new JavaScript runtime',
    },
  ],
  [
    '64734573-ce54-435b-8528-106ac03a0e11',
    {
      id: '64734573-ce54-435b-8528-106ac03a0e11',
      title: 'Common software engineering patterns',
    },
  ],
])
 
export const handlers = [
  graphql.query('ListPosts', () => {
    return HttpResponse.json({
      data: {
        // Convert all posts to an array
        // and return as the "posts" root-level property.
        posts: Array.from(allPosts.values()),
      },
    })
  }),
]

Reading variables

In the mutation CreatePost handler, let’s access the new post we are trying to create and add it to the list of the existing posts. To do that, we need to access a mutation variable representing the next post. Similar to mocking GraphQL responses, accessing variables will depend on how the operation is defined.

Consider the following definition for the CreatePost mutation on the client:

mutation CreatePost($post: PostInput!) {
  createPost(post: $post) {
    id
  }
}

The client creates a $post variable on the mutation of type PostInput that represents a post to be added. You can access operation variables by their name in the variable argument to the response resolver.

Read the mutation variables using the variables object:

export const handlers = [
  graphql.mutation('CreatePost', ({ variables }) => {
    // Read the "post" variable on the mutation.
    const { post } = variables
 
    // Push the new post to the list of all posts.
    allPosts.set(post.id, post)
 
    // Respond with the body matching the mutation.
    return HttpResponse.json({
      createPost: {
        id: post.id,
      },
    })
  }),
]

In the same manner, here’s an example of the DeletePost mutation declaration and its request handler:

mutation DeletePost($postId: ID!) {
  deletePost(id: $postId) {
    id
  }
}
export const handlers = [
  graphql.mutation('DeletePost', ({ variables }) => {
    const { postId } = variables
    const deletedPost = allPosts.get(postId)
 
    // Respond with a GraphQL error when trying
    // to delete a post that doesn't exist.
    if (!deletedPost) {
      return HttpResponse.json({
        errors: [
          {
            message: `Cannot find post with ID "${postId}"`,
          },
        ],
      })
    }
 
    allPosts.delete(postId)
 
    return HttpResponse.json({
      deletePost: deletedPost,
    })
  }),
]

Reading queries

You can access the original query definition set from the client as the query key on the response resolver argument.

graphql.query('ListPosts', ({ query, variables }) => {
  // resolve the request
})

The query object is useful if you want to resolve the intercepted GraphQL operations against a mock GraphQL schema.


One of the reasons to adopt GraphQL is to fetch only the data the client needs. When mocking the GraphQL responses with MSW, however, the entire mock data will be sent to the client as-is, regardless of the fields it queries.

For example, if the client decides to drop the title field on the ListPosts query, it will still receive that field if it’s sent in the mock response. That is because MSW does no field matching that a regular GraphQL server would do when resolving the incoming queries.

You can match the behavior of an actual GraphQL server more closely by resolving the intercepted queries using the graphql package. This approach requires you to define the GraphQL schema and forward the operation name, query, and variables of the intercepted GraphQL operations to be resolved against a mock root value.

Read the operation name and query from the resolver:

import { graphql as executeGraphQL, buildSchema } from 'graphql'
import { graphql, HttpResponse } from 'msw'
 
const schema = buildSchema(`
  type Post {
    id: ID!
    title: String!
  }
 
  type Query {
    posts: [Post!]
  }
`)
 
const allPosts = new Map([
  /* posts here */
])
 
export const handlers = [
  graphql.query('ListPosts', async ({ query, variables }) => {
    const { errors, data } = await executeGraphQL({
      schema,
      source: query,
      variableValues: variables,
      rootValue: {
        posts: Array.from(allPosts.values()),
      },
    })
 
    return HttpResponse.json({ errors, data })
  }),
]

Next steps

Once you have described the network you want, integrate it into any environment in your application.