Mocking GraphQL Operations: Hand-Wired Routes vs a Schema-Driven Mock
Quick answer: GraphQL goes over a single endpoint, so mocking it as a handful of hand-wired REST-style routes misses the point: a client selects different fields and operations on the same URL. A schema-driven mock resolves types and fields from the GraphQL schema, which keeps the response aligned with what the client actually requested and catches a field that the mock forgot to provide. Use hand-wired routes only for a narrow, fixed response; treat the schema as the source of truth for the real mock.
The distinction became clear on a frontend that mocked GraphQL by intercepting the endpoint and returning a saved object. It worked for the one screen the object was built for. When another component reused the same operation but selected a different field set, the mock returned data the client never asked for and omitted fields it did ask for, producing undefined values that looked like a real backend bug. The saved response was not tied to the query at all, so it could not adapt.
Why GraphQL does not map to REST routes
In REST, routes and resources tend to line up, so a per-route mock feels natural. In GraphQL, every request posts to something like /graphql, and the operation lives inside the body, including the exact selection of fields. Two requests to the same URL can expect different shapes.
POST /graphql
{
"query": "query GetOrder($id: ID!) { order(id: $id) { id total status items { name } } }",
"variables": { "id": "ord_42" }
}
A faithful mock has to understand the operation and the selection, not just the URL. That is why a schema-driven mock, which resolves each field according to its type, is the approach that scales.
Hand-wired routes versus a schema-driven mock
The two behave differently as soon as the client changes what it selects. The table below captures the practical difference.
| Concern | Hand-wired route responses | Schema-driven mock |
|---|---|---|
| Field selection | Returns a fixed object regardless of query | Resolves only the fields the client selected |
| Multiple operations | Needs separate logic per operation | Built from one schema and resolvers |
| Schema change | Stale fixtures drift silently | Missing resolver is immediately visible |
| Nullability | Easy to get wrong | Type-aware defaults and null handling |
| Maintenance | Grows with every screen | Shared resolvers reused across queries |
Building a schema-driven mock
A schema-driven mock takes the type definitions and pairs each type with resolver functions or sensible defaults. Shared resolvers return realistic data that any operation selecting that type can reuse.
const mocks = {
Order: () => ({
id: "ord_42",
total: () => 129.99,
status: () => "PROCESSING",
items: () => [{ name: "Cable", quantity: 1 }],
}),
Mutation: () => ({
updateOrderStatus: (_, { id, status }) => ({ id, status }),
}),
};
// The server resolves the query against the schema and the mock map
const server = new GraphQLServer({ typeDefs, mocks });
Because the mock resolves fields through the schema, an operation selecting a new field either gets a resolver or fails loudly in development, instead of quietly receiving an undefined value.
Model the error and loading states too
Do not stop at the happy path. A useful mock lets you return a GraphQL error, a partial response with errors, and a delayed response so the UI is tested for those states. Keep the error shape consistent with what the real gateway returns, and assert the client distinguishes a field-level error from a complete request failure. That is the part that a fixed saved object cannot represent.
Resolve lists and pagination the way the schema describes
List fields are where static objects get it wrong most often. If the schema returns a connection with edges and a page info, the mock should resolve that structure rather than a bare array, and the resolver should return a stable, bounded number of items so pagination tests are deterministic. Reuse the same list resolver across operations that select the type, and provide realistic page info so the client's next-page logic can be exercised. When the real API uses a different list convention, matching the schema exactly prevents the mock from teaching the frontend to expect a shape the server will never send.
Keep the mock tied to the contract
The strongest setup derives the mock directly from the same schema the server uses, so there is no separate fixture to drift. When the schema gains a field, add a resolver in the same change; when a field is removed, the mock no longer exposes it. That keeps mocking a contract exercise rather than a collection of saved objects, which is what made the earlier mock report phantom bugs.
FAQ
Can I just return a saved JSON object for the GraphQL endpoint?
It works only for one fixed query and screen. Since the client selects different fields on the same URL, a fixed object ignores the selection and can omit requested fields. A schema-driven mock resolves the actual query.
Why does the mock return fields the client did not ask for?
Because a hand-wired response is a static object unconnected to the query. A schema-aware mock resolves only the fields in the operation's selection, so the response matches the request.
How do I test GraphQL errors?
Configure the mock to return field-level errors, partial responses, and delayed responses, using the same error shape as the real gateway, and assert the UI handles each state rather than only the happy path.
How do I prevent the mock from drifting?
Build it from the same schema the server uses and add resolvers alongside schema changes. A missing resolver then fails loudly instead of a stale fixture silently returning the wrong shape.
This is part of the Powerduck vs Mockoon hub — start there for the full comparison of OpenAPI mock servers.