0
# GraphQL Handlers
1
2
GraphQL handlers provide powerful interception and mocking of GraphQL operations with support for operation-specific handling, typed responses, and scoped endpoint management.
3
4
## Capabilities
5
6
### GraphQL Request Handler Function
7
8
Creates handlers for GraphQL operations based on operation name, type, or custom predicates.
9
10
```typescript { .api }
11
/**
12
* Creates a GraphQL request handler for specific operations
13
* @param predicate - Operation name, DocumentNode, or custom predicate function
14
* @param resolver - Function that returns a Response for matching GraphQL operations
15
* @param options - Optional configuration including 'once' for single-use handlers
16
* @returns GraphQLHandler instance
17
*/
18
interface GraphQLRequestHandler {
19
<Query extends GraphQLQuery = GraphQLQuery,
20
Variables extends GraphQLVariables = GraphQLVariables>(
21
predicate:
22
| GraphQLHandlerNameSelector
23
| DocumentNode
24
| TypedDocumentNode<Query, Variables>
25
| GraphQLCustomPredicate,
26
resolver: GraphQLResponseResolver<
27
[Query] extends [never] ? GraphQLQuery : Query,
28
Variables
29
>,
30
options?: RequestHandlerOptions
31
): GraphQLHandler;
32
}
33
34
type GraphQLHandlerNameSelector = string;
35
36
type GraphQLCustomPredicate = (info: {
37
query: any;
38
variables: Record<string, any>;
39
operationType: string;
40
operationName?: string;
41
}) => boolean;
42
43
type GraphQLResponseResolver<
44
Query extends GraphQLQuery = GraphQLQuery,
45
Variables extends GraphQLVariables = GraphQLVariables
46
> = (info: GraphQLResolverExtras<Variables>) =>
47
| Response
48
| Promise<Response>
49
| HttpResponse<GraphQLResponseBody<Query>>
50
| Promise<HttpResponse<GraphQLResponseBody<Query>>>;
51
52
interface GraphQLResolverExtras<Variables> {
53
query: any;
54
variables: Variables;
55
operationType: string;
56
operationName?: string;
57
request: Request;
58
}
59
```
60
61
### GraphQL Operation Handlers
62
63
The `graphql` namespace provides handlers for different types of GraphQL operations.
64
65
```typescript { .api }
66
const graphql: {
67
/** Handle GraphQL queries by operation name */
68
query: GraphQLRequestHandler;
69
/** Handle GraphQL mutations by operation name */
70
mutation: GraphQLRequestHandler;
71
/** Handle any GraphQL operation regardless of type or name */
72
operation: <Query, Variables>(
73
resolver: GraphQLResponseResolver<Query, Variables>
74
) => GraphQLHandler;
75
/** Create scoped GraphQL handlers for a specific endpoint URL */
76
link: (url: string) => typeof graphql;
77
};
78
```
79
80
**Usage Examples:**
81
82
```typescript
83
import { graphql, HttpResponse } from "msw";
84
85
// Handle specific query by name
86
graphql.query('GetUser', ({ variables }) => {
87
return HttpResponse.json({
88
data: {
89
user: {
90
id: variables.userId,
91
name: 'John Doe',
92
email: 'john@example.com'
93
}
94
}
95
});
96
});
97
98
// Handle specific mutation by name
99
graphql.mutation('CreatePost', ({ variables }) => {
100
return HttpResponse.json({
101
data: {
102
createPost: {
103
id: Date.now().toString(),
104
title: variables.title,
105
content: variables.content,
106
author: variables.authorId
107
}
108
}
109
});
110
});
111
112
// Handle any GraphQL operation
113
graphql.operation(({ query, variables, operationType, operationName }) => {
114
console.log(`${operationType}: ${operationName}`, variables);
115
116
return HttpResponse.json({
117
data: {},
118
extensions: {
119
tracing: { version: 1, startTime: Date.now() }
120
}
121
});
122
});
123
124
// Handle operations with errors
125
graphql.query('GetSecretData', ({ variables }) => {
126
return HttpResponse.json({
127
errors: [
128
{
129
message: 'Unauthorized access',
130
extensions: {
131
code: 'UNAUTHENTICATED',
132
path: ['secretData']
133
}
134
}
135
],
136
data: null
137
});
138
});
139
140
// Handle operations with partial data and errors
141
graphql.query('GetUserProfile', ({ variables }) => {
142
return HttpResponse.json({
143
data: {
144
user: {
145
id: variables.userId,
146
name: 'John Doe',
147
email: null // Simulating null field
148
}
149
},
150
errors: [
151
{
152
message: 'Email field is restricted',
153
path: ['user', 'email'],
154
extensions: { code: 'FORBIDDEN' }
155
}
156
]
157
});
158
});
159
```
160
161
### Scoped GraphQL Handlers
162
163
Create handlers for specific GraphQL endpoints using the `link` method.
164
165
```typescript { .api }
166
/**
167
* Creates scoped GraphQL handlers for a specific endpoint URL
168
* @param url - The GraphQL endpoint URL to scope handlers to
169
* @returns Object with query, mutation, and operation methods scoped to the URL
170
*/
171
function link(url: string): {
172
query: GraphQLRequestHandler;
173
mutation: GraphQLRequestHandler;
174
operation: GraphQLRequestHandler;
175
};
176
```
177
178
**Usage Examples:**
179
180
```typescript
181
// Create scoped handlers for different GraphQL APIs
182
const githubApi = graphql.link('https://api.github.com/graphql');
183
const shopifyApi = graphql.link('https://shop.myshopify.com/api/graphql');
184
185
// GitHub API handlers
186
githubApi.query('GetRepository', ({ variables }) => {
187
return HttpResponse.json({
188
data: {
189
repository: {
190
name: variables.name,
191
owner: { login: variables.owner },
192
stargazerCount: 1234
193
}
194
}
195
});
196
});
197
198
// Shopify API handlers
199
shopifyApi.query('GetProducts', () => {
200
return HttpResponse.json({
201
data: {
202
products: {
203
edges: [
204
{ node: { id: '1', title: 'Product 1', handle: 'product-1' } }
205
]
206
}
207
}
208
});
209
});
210
211
// Local development API
212
const localApi = graphql.link('http://localhost:4000/graphql');
213
214
localApi.mutation('SignUp', ({ variables }) => {
215
return HttpResponse.json({
216
data: {
217
signUp: {
218
token: 'fake-jwt-token',
219
user: {
220
id: 'new-user-id',
221
email: variables.email
222
}
223
}
224
}
225
});
226
});
227
```
228
229
### Typed GraphQL Handlers
230
231
Use TypeScript for type-safe GraphQL handlers with DocumentNode or TypedDocumentNode.
232
233
```typescript { .api }
234
interface TypedDocumentNode<
235
Result = { [key: string]: any },
236
Variables = { [key: string]: any }
237
> extends DocumentNode {
238
__apiType?: (variables: Variables) => Result;
239
__resultType?: Result;
240
__variablesType?: Variables;
241
}
242
```
243
244
**Usage Examples:**
245
246
```typescript
247
import { graphql, HttpResponse } from "msw";
248
import { DocumentNode } from "graphql";
249
250
// Using DocumentNode (from graphql-tag, @apollo/client, etc.)
251
const GET_USER_QUERY: DocumentNode = gql`
252
query GetUser($userId: ID!) {
253
user(id: $userId) {
254
id
255
name
256
257
}
258
}
259
`;
260
261
graphql.query(GET_USER_QUERY, ({ variables }) => {
262
return HttpResponse.json({
263
data: {
264
user: {
265
id: variables.userId,
266
name: 'John Doe',
267
email: 'john@example.com'
268
}
269
}
270
});
271
});
272
273
// Using TypedDocumentNode for full type safety
274
interface GetUserQuery {
275
user: {
276
id: string;
277
name: string;
278
email: string;
279
};
280
}
281
282
interface GetUserVariables {
283
userId: string;
284
}
285
286
const TYPED_GET_USER_QUERY: TypedDocumentNode<GetUserQuery, GetUserVariables> = gql`
287
query GetUser($userId: ID!) {
288
user(id: $userId) {
289
id
290
name
291
292
}
293
}
294
`;
295
296
graphql.query(TYPED_GET_USER_QUERY, ({ variables }) => {
297
// variables is fully typed as GetUserVariables
298
// Return type is enforced to match GetUserQuery
299
return HttpResponse.json({
300
data: {
301
user: {
302
id: variables.userId, // TypeScript knows this is string
303
name: 'John Doe',
304
email: 'john@example.com'
305
}
306
}
307
});
308
});
309
```
310
311
### Custom GraphQL Predicates
312
313
Use custom predicate functions for advanced operation matching.
314
315
```typescript
316
// Match operations by complexity
317
graphql.operation(({ query, variables }) => {
318
return HttpResponse.json({ data: {} });
319
}, {
320
predicate: ({ query, operationType }) => {
321
// Custom logic to determine if this handler should match
322
return operationType === 'query' && query.definitions.length > 1;
323
}
324
});
325
326
// Match operations with specific variables
327
graphql.query('GetUser', ({ variables }) => {
328
return HttpResponse.json({
329
data: { user: { id: variables.userId, name: 'Admin User' } }
330
});
331
}, {
332
predicate: ({ variables }) => {
333
return variables.userId === 'admin';
334
}
335
});
336
337
// Match operations by request headers
338
graphql.operation(({ request }) => {
339
const authHeader = request.headers.get('authorization');
340
341
if (!authHeader?.startsWith('Bearer ')) {
342
return HttpResponse.json({
343
errors: [{ message: 'Authentication required' }]
344
}, { status: 401 });
345
}
346
347
return HttpResponse.json({ data: {} });
348
}, {
349
predicate: ({ request }) => {
350
return !request.headers.get('authorization');
351
}
352
});
353
```
354
355
### GraphQL Response Formats
356
357
Handle different GraphQL response patterns including data, errors, and extensions.
358
359
```typescript
360
// Successful response with data
361
graphql.query('GetUser', () => {
362
return HttpResponse.json({
363
data: {
364
user: { id: '1', name: 'John' }
365
}
366
});
367
});
368
369
// Response with errors
370
graphql.mutation('DeleteUser', () => {
371
return HttpResponse.json({
372
errors: [
373
{
374
message: 'User not found',
375
extensions: {
376
code: 'USER_NOT_FOUND',
377
exception: {
378
stacktrace: ['Error: User not found', ' at...']
379
}
380
},
381
locations: [{ line: 2, column: 3 }],
382
path: ['deleteUser']
383
}
384
],
385
data: { deleteUser: null }
386
});
387
});
388
389
// Response with extensions
390
graphql.query('GetPosts', () => {
391
return HttpResponse.json({
392
data: {
393
posts: [{ id: '1', title: 'Hello World' }]
394
},
395
extensions: {
396
tracing: {
397
version: 1,
398
startTime: '2023-01-01T00:00:00Z',
399
endTime: '2023-01-01T00:00:01Z',
400
duration: 1000000000
401
},
402
caching: {
403
version: 1,
404
hints: [{ path: ['posts'], maxAge: 300 }]
405
}
406
}
407
});
408
});
409
410
// HTTP error response (network/server error)
411
graphql.operation(() => {
412
return HttpResponse.json(
413
{ error: 'Internal Server Error' },
414
{ status: 500 }
415
);
416
});
417
```
418
419
## Types
420
421
```typescript { .api }
422
// Handler types
423
interface GraphQLHandler {
424
operationType: GraphQLOperationType;
425
predicate:
426
| string
427
| DocumentNode
428
| TypedDocumentNode<any, any>
429
| GraphQLCustomPredicate;
430
url: string;
431
resolver: GraphQLResponseResolver<any, any>;
432
options: RequestHandlerOptions;
433
}
434
435
type GraphQLOperationType = 'query' | 'mutation' | 'subscription' | 'all';
436
437
// Request types
438
type GraphQLQuery = Record<string, any>;
439
type GraphQLVariables = Record<string, any>;
440
441
interface GraphQLRequestBody {
442
query: string;
443
variables?: GraphQLVariables;
444
operationName?: string;
445
}
446
447
interface GraphQLJsonRequestBody extends GraphQLRequestBody {
448
extensions?: Record<string, any>;
449
}
450
451
// Response types
452
type GraphQLResponseBody<Query extends GraphQLQuery = GraphQLQuery> = {
453
data?: Query;
454
errors?: GraphQLError[];
455
extensions?: Record<string, any>;
456
};
457
458
interface GraphQLError {
459
message: string;
460
locations?: Array<{ line: number; column: number }>;
461
path?: Array<string | number>;
462
extensions?: Record<string, any>;
463
}
464
465
// Operation parsing
466
interface ParsedGraphQLRequest {
467
operationType: string;
468
operationName?: string;
469
query: any;
470
variables: GraphQLVariables;
471
}
472
```