Seamless REST/GraphQL API mocking library for browser and Node.js.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
GraphQL handlers provide powerful interception and mocking of GraphQL operations with support for operation-specific handling, typed responses, and scoped endpoint management.
Creates handlers for GraphQL operations based on operation name, type, or custom predicates.
/**
* Creates a GraphQL request handler for specific operations
* @param predicate - Operation name, DocumentNode, or custom predicate function
* @param resolver - Function that returns a Response for matching GraphQL operations
* @param options - Optional configuration including 'once' for single-use handlers
* @returns GraphQLHandler instance
*/
interface GraphQLRequestHandler {
<Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables>(
predicate:
| GraphQLHandlerNameSelector
| DocumentNode
| TypedDocumentNode<Query, Variables>
| GraphQLCustomPredicate,
resolver: GraphQLResponseResolver<
[Query] extends [never] ? GraphQLQuery : Query,
Variables
>,
options?: RequestHandlerOptions
): GraphQLHandler;
}
type GraphQLHandlerNameSelector = string;
type GraphQLCustomPredicate = (info: {
query: any;
variables: Record<string, any>;
operationType: string;
operationName?: string;
}) => boolean;
type GraphQLResponseResolver<
Query extends GraphQLQuery = GraphQLQuery,
Variables extends GraphQLVariables = GraphQLVariables
> = (info: GraphQLResolverExtras<Variables>) =>
| Response
| Promise<Response>
| HttpResponse<GraphQLResponseBody<Query>>
| Promise<HttpResponse<GraphQLResponseBody<Query>>>;
interface GraphQLResolverExtras<Variables> {
query: any;
variables: Variables;
operationType: string;
operationName?: string;
request: Request;
}The graphql namespace provides handlers for different types of GraphQL operations.
const graphql: {
/** Handle GraphQL queries by operation name */
query: GraphQLRequestHandler;
/** Handle GraphQL mutations by operation name */
mutation: GraphQLRequestHandler;
/** Handle any GraphQL operation regardless of type or name */
operation: <Query, Variables>(
resolver: GraphQLResponseResolver<Query, Variables>
) => GraphQLHandler;
/** Create scoped GraphQL handlers for a specific endpoint URL */
link: (url: string) => typeof graphql;
};Usage Examples:
import { graphql, HttpResponse } from "msw";
// Handle specific query by name
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.userId,
name: 'John Doe',
email: 'john@example.com'
}
}
});
});
// Handle specific mutation by name
graphql.mutation('CreatePost', ({ variables }) => {
return HttpResponse.json({
data: {
createPost: {
id: Date.now().toString(),
title: variables.title,
content: variables.content,
author: variables.authorId
}
}
});
});
// Handle any GraphQL operation
graphql.operation(({ query, variables, operationType, operationName }) => {
console.log(`${operationType}: ${operationName}`, variables);
return HttpResponse.json({
data: {},
extensions: {
tracing: { version: 1, startTime: Date.now() }
}
});
});
// Handle operations with errors
graphql.query('GetSecretData', ({ variables }) => {
return HttpResponse.json({
errors: [
{
message: 'Unauthorized access',
extensions: {
code: 'UNAUTHENTICATED',
path: ['secretData']
}
}
],
data: null
});
});
// Handle operations with partial data and errors
graphql.query('GetUserProfile', ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.userId,
name: 'John Doe',
email: null // Simulating null field
}
},
errors: [
{
message: 'Email field is restricted',
path: ['user', 'email'],
extensions: { code: 'FORBIDDEN' }
}
]
});
});Create handlers for specific GraphQL endpoints using the link method.
/**
* Creates scoped GraphQL handlers for a specific endpoint URL
* @param url - The GraphQL endpoint URL to scope handlers to
* @returns Object with query, mutation, and operation methods scoped to the URL
*/
function link(url: string): {
query: GraphQLRequestHandler;
mutation: GraphQLRequestHandler;
operation: GraphQLRequestHandler;
};Usage Examples:
// Create scoped handlers for different GraphQL APIs
const githubApi = graphql.link('https://api.github.com/graphql');
const shopifyApi = graphql.link('https://shop.myshopify.com/api/graphql');
// GitHub API handlers
githubApi.query('GetRepository', ({ variables }) => {
return HttpResponse.json({
data: {
repository: {
name: variables.name,
owner: { login: variables.owner },
stargazerCount: 1234
}
}
});
});
// Shopify API handlers
shopifyApi.query('GetProducts', () => {
return HttpResponse.json({
data: {
products: {
edges: [
{ node: { id: '1', title: 'Product 1', handle: 'product-1' } }
]
}
}
});
});
// Local development API
const localApi = graphql.link('http://localhost:4000/graphql');
localApi.mutation('SignUp', ({ variables }) => {
return HttpResponse.json({
data: {
signUp: {
token: 'fake-jwt-token',
user: {
id: 'new-user-id',
email: variables.email
}
}
}
});
});Use TypeScript for type-safe GraphQL handlers with DocumentNode or TypedDocumentNode.
interface TypedDocumentNode<
Result = { [key: string]: any },
Variables = { [key: string]: any }
> extends DocumentNode {
__apiType?: (variables: Variables) => Result;
__resultType?: Result;
__variablesType?: Variables;
}Usage Examples:
import { graphql, HttpResponse } from "msw";
import { DocumentNode } from "graphql";
// Using DocumentNode (from graphql-tag, @apollo/client, etc.)
const GET_USER_QUERY: DocumentNode = gql`
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
}
}
`;
graphql.query(GET_USER_QUERY, ({ variables }) => {
return HttpResponse.json({
data: {
user: {
id: variables.userId,
name: 'John Doe',
email: 'john@example.com'
}
}
});
});
// Using TypedDocumentNode for full type safety
interface GetUserQuery {
user: {
id: string;
name: string;
email: string;
};
}
interface GetUserVariables {
userId: string;
}
const TYPED_GET_USER_QUERY: TypedDocumentNode<GetUserQuery, GetUserVariables> = gql`
query GetUser($userId: ID!) {
user(id: $userId) {
id
name
email
}
}
`;
graphql.query(TYPED_GET_USER_QUERY, ({ variables }) => {
// variables is fully typed as GetUserVariables
// Return type is enforced to match GetUserQuery
return HttpResponse.json({
data: {
user: {
id: variables.userId, // TypeScript knows this is string
name: 'John Doe',
email: 'john@example.com'
}
}
});
});Use custom predicate functions for advanced operation matching.
// Match operations by complexity
graphql.operation(({ query, variables }) => {
return HttpResponse.json({ data: {} });
}, {
predicate: ({ query, operationType }) => {
// Custom logic to determine if this handler should match
return operationType === 'query' && query.definitions.length > 1;
}
});
// Match operations with specific variables
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: { user: { id: variables.userId, name: 'Admin User' } }
});
}, {
predicate: ({ variables }) => {
return variables.userId === 'admin';
}
});
// Match operations by request headers
graphql.operation(({ request }) => {
const authHeader = request.headers.get('authorization');
if (!authHeader?.startsWith('Bearer ')) {
return HttpResponse.json({
errors: [{ message: 'Authentication required' }]
}, { status: 401 });
}
return HttpResponse.json({ data: {} });
}, {
predicate: ({ request }) => {
return !request.headers.get('authorization');
}
});Handle different GraphQL response patterns including data, errors, and extensions.
// Successful response with data
graphql.query('GetUser', () => {
return HttpResponse.json({
data: {
user: { id: '1', name: 'John' }
}
});
});
// Response with errors
graphql.mutation('DeleteUser', () => {
return HttpResponse.json({
errors: [
{
message: 'User not found',
extensions: {
code: 'USER_NOT_FOUND',
exception: {
stacktrace: ['Error: User not found', ' at...']
}
},
locations: [{ line: 2, column: 3 }],
path: ['deleteUser']
}
],
data: { deleteUser: null }
});
});
// Response with extensions
graphql.query('GetPosts', () => {
return HttpResponse.json({
data: {
posts: [{ id: '1', title: 'Hello World' }]
},
extensions: {
tracing: {
version: 1,
startTime: '2023-01-01T00:00:00Z',
endTime: '2023-01-01T00:00:01Z',
duration: 1000000000
},
caching: {
version: 1,
hints: [{ path: ['posts'], maxAge: 300 }]
}
}
});
});
// HTTP error response (network/server error)
graphql.operation(() => {
return HttpResponse.json(
{ error: 'Internal Server Error' },
{ status: 500 }
);
});// Handler types
interface GraphQLHandler {
operationType: GraphQLOperationType;
predicate:
| string
| DocumentNode
| TypedDocumentNode<any, any>
| GraphQLCustomPredicate;
url: string;
resolver: GraphQLResponseResolver<any, any>;
options: RequestHandlerOptions;
}
type GraphQLOperationType = 'query' | 'mutation' | 'subscription' | 'all';
// Request types
type GraphQLQuery = Record<string, any>;
type GraphQLVariables = Record<string, any>;
interface GraphQLRequestBody {
query: string;
variables?: GraphQLVariables;
operationName?: string;
}
interface GraphQLJsonRequestBody extends GraphQLRequestBody {
extensions?: Record<string, any>;
}
// Response types
type GraphQLResponseBody<Query extends GraphQLQuery = GraphQLQuery> = {
data?: Query;
errors?: GraphQLError[];
extensions?: Record<string, any>;
};
interface GraphQLError {
message: string;
locations?: Array<{ line: number; column: number }>;
path?: Array<string | number>;
extensions?: Record<string, any>;
}
// Operation parsing
interface ParsedGraphQLRequest {
operationType: string;
operationName?: string;
query: any;
variables: GraphQLVariables;
}