GraphQL Mock Data: Templating Responses That Match Your Query Shape
A REST endpoint returns a fixed shape: GET /users/123 always comes back
with the same fields, whether the client needs three of them or all twenty.
GraphQL doesn't work that way. The same User type can come back as a bare
{ id, name } in one query and a five-level-deep tree of orders, line
items, and addresses in another, because the client's selection set decides
the shape, not the server. That's exactly what makes generating GraphQL
mock data different from mocking a REST response: there's no one fixed
schema for "a user" to fake — there's a different shape per query, and your
mock has to match whichever one you're testing against.
The fix is mechanical once you see it: write a JSON template that mirrors
your query's selection set, field for field and nesting level for nesting
level, and let JsonFabrica fill it with generated values. This post walks
through that technique for nested types, Relay-style pagination, and wiring
consistent IDs across a tree — then shows the generated JSON dropping into
Apollo's MockedProvider, @graphql-tools/mock, and msw's graphql
handlers.
Templating GraphQL mock data to match the query's shape
Say your client sends this query:
query GetOrder($id: ID!) {
order(id: $id) {
id
total
customer {
id
name
email
}
items {
sku
quantity
}
}
}
The mock has to be a JSON object with an order key, itself an object with
id, total, a nested customer object, and an items array — the same
tree the query asks for, no more and no less. A JsonFabrica template for
that shape looks like this:
{
"order": {
"id": "<createSeq('orderId', 'number', 1000, 1)><getSeq('orderId')>",
"total": "<getRandomNumber(20, 500, 2)>",
"customer": {
"id": "<createSeq('customerId', 'number', 1, 1)><getSeq('customerId')>",
"name": "<getRandomFullName()>",
"email": "<getRandomEmail()>"
},
"items": [<for(i, 0, 2)>{
"sku": "<getRandomTextWithSpaces(6,6)>",
"quantity": "<getRandomNumber(1, 10)>"
},<end_for>]
}
}
That's the whole technique for mock GraphQL responses: whatever your
query selects, your template has the matching key at the matching depth.
Add a field to the selection set, add it to the template. Query a
shippingAddress object nested three levels down, nest an object three
levels down in the template. There's no schema-aware step in between —
you're copying the query's shape by hand into JSON, then swapping literal
values for functions like getRandomFullName(), getRandomEmail(), and
createSeq/getSeq (createSeq('orderId', 'number', 1000, 1) declares an
incrementing counter once, getSeq('orderId') draws the next value each
call so order IDs never collide). getRandomNumber(min, max, decimals) and
getRandomTextWithSpaces(minLen, maxLen) fill in the numeric and string
leaves the same way they would in any other template.
Keeping nested-type IDs consistent
A single template call is fine for one order with its own inline customer,
but it breaks down the moment two different queries need to agree on the
same customer — a GetOrder query and a separate GetCustomer query
both returning customer.id: "cus_42", say, for a test that checks a UI
correctly links the two. Templates generated independently produce
independent random IDs; they don't line up.
JsonFabrica's batch endpoint solves this the same way it does for REST
fixtures: generate every type the tree touches in one batch call and wire
the child field to the parent with a relations entry.
{
"documents": [
{ "templateId": "tpl_customer", "alias": "customer", "count": 10 },
{
"templateId": "tpl_order",
"alias": "order",
"count": 25,
"relations": {
"customerId": { "from": "customer.id", "strategy": "round-robin" }
}
}
]
}
Two forms are valid inside relations. The object form shown above,
{ from: 'customer.id', strategy: 'round-robin' }, points a field at one
specific field on a specific parent alias. A bare string — just
"customer" instead of the object — points the field at the whole parent
document rather than a single field of it. round-robin is the only
strategy value the API accepts; anything else in that field throws an
error rather than silently falling back to something else. Point your
order template's customer object at customer.id and customer.name
this way and every order in the batch references a customer that actually
exists elsewhere in the same response — the property a UI test asserting
on cross-type consistency actually needs.
Templating Relay-style connections
Paginated GraphQL lists are almost always shaped as a connection: an
edges array of { node, cursor } pairs, plus a pageInfo object.
A query like:
query Orders($first: Int!, $after: String) {
orders(first: $first, after: $after) {
edges {
node { id total }
cursor
}
pageInfo {
hasNextPage
endCursor
}
}
}
templates directly, with the same nesting the query asks for:
{
"orders": {
"edges": [<for(i, 0, 9)>{
"node": {
"id": "<createSeq('orderId', 'number', 1000, 1)><getSeq('orderId')>",
"total": "<getRandomNumber(20, 500, 2)>"
},
"cursor": "<createSeq('cursor', 'uuid')><getSeq('cursor', 'cursor')>"
},<end_for>],
"pageInfo": {
"hasNextPage": "<getRandomBoolean(0.8)>",
"endCursor": "<getVar('cursor')>"
}
}
}
The edges array is a for(i, start, end) / end_for loop, same as
building any repeated array in a JsonFabrica template — for(i, 0, 9)
emits 10 edges since both bounds are inclusive. getRandomBoolean(trueProbability)
takes a 0–1 probability rather than a percentage, so getRandomBoolean(0.8)
skews hasNextPage true 80% of the time, useful for testing a "load more"
path more often than the terminal page. The cursor field declares a
uuid-typed sequence once with createSeq('cursor', 'uuid'), then
getSeq('cursor', 'cursor') draws the next value on each edge and, via
that optional second argument most functions accept, stores it under a
variable of the same name; endCursor then reads back whichever cursor was
generated last with getVar('cursor'). There's nothing GraphQL-specific
inside JsonFabrica making this work — edges, node, and pageInfo are
just regular nested JSON keys you wrote out because that's the shape the
Relay connection spec expects, and JsonFabrica fills whatever nested JSON
shape you hand it.
Wiring generated data into your mocking layer
The generated JSON is a plain object matching your query's shape, so it plugs into whatever mocking layer your test stack already uses without any adapter step.
Apollo Client's MockedProvider wants a mocks array of
{ request, result } pairs — drop the generated object straight in as
result.data:
const mocks = [
{
request: { query: GET_ORDER, variables: { id: "1001" } },
result: { data: generatedOrderResponse },
},
];
render(
<MockedProvider mocks={mocks}>
<OrderPage id="1001" />
</MockedProvider>
);
@graphql-tools/mock takes generated values through custom mock
resolvers rather than a raw response object, but you can still hand it a
pre-generated record per type and have the resolver just return it:
const mocks = {
Order: () => generatedOrder,
Customer: () => generatedCustomer,
};
const schema = addMocksToSchema({ schema: baseSchema, mocks });
msw's graphql handlers intercept the operation by name and resolve
it with whatever data object you return — generated JSON fits there
directly too:
import { graphql, HttpResponse } from "msw";
export const handlers = [
graphql.query("GetOrder", () => {
return HttpResponse.json({ data: generatedOrderResponse });
}),
];
In every case the mocking layer's job stops at "return this JSON when this operation runs" — matching the query's shape and keeping cross-type IDs consistent is the part a hand-typed fixture gets tedious and stale, and where generating from a template pays off as your queries keep changing.
FAQ
How do I generate mock data for a GraphQL query?
Write a JSON template whose shape mirrors your query's selection set
exactly, field by field and nesting level by nesting level, using
JsonFabrica's generator functions in place of literal values. Generating
from that template returns a JSON object matching your query's shape,
which you can drop straight into a data key for MockedProvider,
@graphql-tools/mock, or an msw resolver.
Does JsonFabrica generate mock data directly from a GraphQL schema or
.graphql query file?
No. JsonFabrica has no GraphQL endpoint and doesn't parse or introspect
your schema or query documents. You hand-write a JSON template that
mirrors the shape of a specific query's selection set, and JsonFabrica
fills that template with generated values on each call.
How do I keep nested IDs consistent in GraphQL mock data, like an
order's customerId matching a customer's id?
Generate both types in one JsonFabrica batch request and add a relations
entry mapping the child field to the parent, for example
customerId: { from: 'customer.id', strategy: 'round-robin' }.
round-robin is the only relation strategy the API supports; any other
value throws an error. The bare alias form, without the object wrapper,
links a field to a whole parent document instead of one of its fields.
How do I mock a Relay-style GraphQL connection with edges and
pageInfo?
Template the connection as an object with an edges array of
{ node, cursor } pairs built with a for loop, plus a pageInfo object
carrying hasNextPage and endCursor, matching exactly the fields your
query selects under that connection. There's no built-in connection type
in JsonFabrica — you write the edges/node/pageInfo shape as regular nested
JSON like any other part of the template.
Can I use JsonFabrica-generated data with msw's graphql request
handlers?
Yes. Generate the JSON payload ahead of time or fetch it inside the
resolver, then return it as the data field of the response your
graphql.query or graphql.link handler resolves with — msw doesn't care
whether that payload came from a hand-written fixture or a generation API,
only that its shape matches the query.
JsonFabrica doesn't speak GraphQL natively, but it doesn't need to: once you've mirrored a query's shape into a JSON template, the batches API generates and links as many consistent records as that query — or a whole tree of related queries — needs.
Generate realistic test data with JsonFabrica
Describe the shape of your data once, then generate as many fresh, realistic JSON documents as you need via a simple API call.