- Introduction
- Getting started
- Philosophy
- Comparison
- Limitations
- Debugging runbook
- FAQ
- Basics
- Concepts
- Network behavior
- Integrations
- API
- CLI
- Best practices
- Recipes
- Cookies
- Query parameters
- Response patching
- Polling
- Streaming
- Network errors
- File uploads
- Responding with binary
- Custom worker script location
- Global response delay
- GraphQL query batching
- Higher-order resolver
- Keeping mocks in sync
- Merging Service Workers
- Mock GraphQL schema
- Using CDN
- Using custom "homepage" property
- Using local HTTPS
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:
Response resolver
Learn more about response resolvers.
Mocking responses
Learn how to mock HTTP responses.
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 standardResponse
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
}
}
Prefer named queries in order to use the graphql.query()
and
graphql.mutation()
request handlers. For anonymous GraphQL queries, use the
graphql.operation()
API
instead.
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.