Your app posts every GraphQL query to the single endpoint /graphql. In Mock Service Worker (MSW), why would you write graphql.query('GetUser', resolver) rather than an http.post('/graphql', resolver) handler?
answer
- the URL no longer identifies anything
- one endpoint, many operations
- identity moved into the payload
- match on type and name
- you still write the data envelope
basics
~20 sEvery GraphQL call is a POST to the same URL, so method-plus-path cannot tell GetUser from GetOrders. MSW's graphql handlers read the operation out of the request and match on its type and name, giving the resolver the variables too.
solid answer
~50 sWith GraphQL the URL stops carrying information: every query and mutation goes to `/graphql` by the same method, so an `http.post('/graphql')` handler claims all of them and you would have to parse the body and branch by hand inside one resolver. MSW's `graphql` namespace does that for you — `graphql.query('GetUser', …)` matches only the query whose operation name is `GetUser`, `graphql.mutation('UpdateUser', …)` only that mutation, and the resolver receives the parsed `variables`. You return the GraphQL envelope yourself with `HttpResponse.json({ data: { … } })`, or an `errors` array to exercise the failure path. Two consequences worth knowing: the operation must be named in the client's document, because an anonymous query has nothing to match on, and if the app talks to more than one GraphQL endpoint you scope handlers to a specific URL with `graphql.link()`.
code
javascript · 9 linesimport { graphql, HttpResponse } from 'msw'
export const handlers = [
graphql.query('GetUser', ({ variables }) => {
return HttpResponse.json({
data: { user: { id: variables.id, name: 'Ada' } },
})
}),
]go deeper
Know that all GraphQL calls share one endpoint, so MSW matches them by operation name with graphql.query and graphql.mutation rather than by URL.
Explain what the handler inspects to find the operation, that variables arrive in the resolver, and that you return the data envelope yourself because no schema is executed.
Show judgment about fidelity: model errors the way a GraphQL server really returns them, scope handlers per endpoint when there are several, and name the drift risk in hand-written envelopes.
Frame the choice of matching level as a protocol question — identity lives in the URL for REST and in the payload for GraphQL — and set the team convention for where GraphQL fixtures come from and who keeps them honest.
## The URL stops being the identity REST puts the identity of an operation in the request line: `GET /api/users/7` says what you want and what you intend to do with it. That is exactly what a network-level mock matches on. GraphQL moves that identity into the request body — one endpoint, almost always `POST /graphql`, with the document and variables in the payload. The routing information a URL-based matcher relies on is gone. So a handler written as `http.post('/graphql', …)` matches every operation the app will ever issue. To make it useful you would read the body, look at the document or the `operationName` field, and branch: ```js http.post('/graphql', async ({ request }) => { const { operationName, variables } = await request.json() if (operationName === 'GetUser') return HttpResponse.json({ data: { user: { id: variables.id } } }) // …a growing else-if chain }) ``` That works, and it is precisely the boilerplate the `graphql` namespace exists to remove. ## What graphql handlers match on ```js import { graphql, HttpResponse } from 'msw' export const handlers = [ graphql.query('GetUser', ({ variables }) => { return HttpResponse.json({ data: { user: { id: variables.id, name: 'Ada' } }, }) }), graphql.mutation('UpdateUser', ({ variables }) => { return HttpResponse.json({ data: { updateUser: { id: variables.id } } }) }), ] ``` MSW inspects the intercepted request, extracts the operation, and matches on two things: the operation *type* (query or mutation) and the operation *name*. The resolver receives the parsed `variables`, which is usually what a test wants to assert on — that the component sent the right id, the right page size, the right filter. `graphql.operation()` matches any operation, which is the catch-all when you want one blanket response or a passthrough. ## You still own the envelope MSW does not execute a schema. It hands back whatever you return, so you must return the GraphQL response envelope yourself — a top-level `data` object shaped like the query's selection set. A common early mistake is returning the entity directly (`HttpResponse.json({ user })`), which the client then reads as a response with no `data` field. The same envelope is how you test failure paths: GraphQL conventionally answers with HTTP 200 and an `errors` array, so a handler returning `HttpResponse.json({ errors: [{ message: 'Not found' }] })` exercises the client's error branch far more faithfully than a 500 would, because a 500 is not what the real server does. Because you are writing the envelope by hand, nothing checks it against the schema — an over-fetched or misspelled field in your fixture will sail through the mock and only fail against the real API. Deriving fixtures from the schema is the countermeasure, and it is a separate discipline from handler matching. ## Two gotchas **Anonymous operations.** A document written as `query { user { id } }` has no name, so a name-based handler has nothing to match. If the client library strips or omits operation names, or a developer writes an anonymous document, the handler silently does not apply. Naming every operation is good practice anyway — it is what makes operations legible in server logs and traces — so the fix is on the client side. **Multiple endpoints.** If the app talks to two GraphQL services, an operation name alone might be ambiguous or, worse, the same name might exist on both. `graphql.link('https://api.example.com/graphql')` returns a scoped namespace whose `query` and `mutation` handlers match only requests to that URL, restoring the endpoint dimension when you actually need it. ## The transferable principle The wider lesson survives any particular mocking tool: **match a request at the level where its identity actually lives.** For REST that is method plus path. For GraphQL it is the operation inside the payload. For an RPC-over-POST API it may be a method field in the body or a header. When your mocking layer only offers URL matching for a protocol whose identity is not in the URL, you end up hand-writing a dispatcher, and that dispatcher becomes an untested piece of test infrastructure with its own bugs. Choosing a matcher that speaks the protocol's own vocabulary keeps the fixture readable and keeps each test's override narrow — you override one operation, not the whole endpoint.
- A GraphQL handler you registered never fires, though the request clearly leaves the app. What do you check?Whether the operation is named: `graphql.query('GetUser')` matches on the operation name, and an anonymous document has none, so nothing matches. Then check the type — a mutation will not match a `graphql.query` handler — and, if the app talks to more than one GraphQL service, whether you need `graphql.link()` to scope the handler to the right endpoint.
- How would you test a component's handling of a GraphQL error with MSW?Return the GraphQL error envelope, not an HTTP failure: `HttpResponse.json({ errors: [{ message: 'Not found' }] })`, optionally with a partial `data`. GraphQL servers conventionally answer errors with HTTP 200, so a 500 would exercise a transport-failure branch the real server never triggers. Testing the branch the server actually produces is the point.
- What does MSW not give you that a real GraphQL server would?Schema execution. MSW returns whatever object your resolver produces, so nothing validates that the fixture matches the schema or that it covers the fields the query selected. A mock that over- or under-fetches passes happily and only breaks against the real API, which is why fixtures are worth deriving from the schema rather than hand-writing.
saying these in an interview costs you the question
- Assumes GraphQL operations are distinguishable by URL path
- Returns the entity directly instead of a data envelope
- Mocks GraphQL errors as HTTP 500 responses
- Forgets anonymous operations cannot be matched by name
- Thinks MSW executes the query against a schema