A CLI tool for generating TypeScript mock factories and MSW handlers from GraphQL fragments and operations. Designed to streamline testing and development workflows in GraphQL-based TypeScript projects.
- Factory Generation: Creates type-safe mock factories from GraphQL fragments
- Collection Factories: Generates companion array factories alongside single-entity factories
- Handler Generation: Generates MSW (Mock Service Worker) handlers with nested response support
- Spy Export: Every handler exports a Storybook-compatible
fn()spy for test assertions - Incremental Updates: Only regenerates files when source fragments or schema change
- Manual Customization: Preserves manual modifications while adding new fields automatically
- Dependency Resolution: Automatically handles nested fragment dependencies
- Storybook Auto-Detection: Detects
storybook/testvs@storybook/testfrom your project's dependencies - Cache Management: Tracks generation history to optimize build times
npm install -g gql-codegen-toolsOr install locally in your project:
npm install --save-dev gql-codegen-toolsGenerate factory files from GraphQL fragments:
gql-codegen-tools factoriesGenerate MSW handlers from GraphQL operations:
gql-codegen-tools handlersThe tool expects your project to follow this structure:
project-root/
├── schema.graphql # GraphQL schema file
├── src/
│ └── gql/
│ ├── ids.ts # Centralized ID constants
│ └── advertiser/
│ ├── fragments/
│ │ ├── advertiser.fragment.gql
│ │ ├── advertiser.fragment.generated.ts
│ │ ├── advertiser.factory.ts # Generated
│ │ └── advertisers.factory.ts # Generated (collection)
│ ├── queries/
│ │ ├── advertisers.query.gql
│ │ ├── advertisers.query.generated.ts
│ │ └── advertisers.handler.ts # Generated
│ └── mutations/
│ ├── create-advertiser.mutation.gql
│ ├── create-advertiser.mutation.generated.ts
│ └── create-advertiser.handler.ts # Generated
Fragment files are discovered at src/**/*.fragment.gql and operation files at src/**/*.{query,mutation}.gql.
Add these scripts to your package.json:
{
"scripts": {
"codegen:factories": "gql-codegen-tools factories",
"codegen:handlers": "gql-codegen-tools handlers",
"codegen": "npm run codegen:factories && npm run codegen:handlers"
}
}# advertiser.fragment.gql
fragment Advertiser on Advertiser {
id
name
industry
website
}// advertiser.factory.ts
import { type AdvertiserFragment } from "./advertiser.fragment.generated";
import { ids } from "../../ids";
const defaultAdvertiser: AdvertiserFragment = {
id: ids.advertiser[0],
name: "lorem",
industry: "Autos & Vehicles",
website: "www.advertiser.com",
__typename: "Advertiser",
};
export const createMockAdvertiser = (overwrites: Partial<AdvertiserFragment> = {}): AdvertiserFragment => ({
...defaultAdvertiser,
...overwrites,
});A companion collection factory is generated alongside each single-entity factory:
// advertisers.factory.ts
import { type AdvertiserFragment } from "./advertiser.fragment.generated";
import { createMockAdvertiser } from "./advertiser.factory";
import { ids } from "../../ids";
const defaultAdvertisers: AdvertiserFragment[] = [
createMockAdvertiser(),
createMockAdvertiser({ id: ids.advertiser[1] }),
];
export const createMockAdvertisers = (overwrites?: AdvertiserFragment[]): AdvertiserFragment[] =>
overwrites ?? defaultAdvertisers;When a fragment references other fragments, the tool automatically generates factories for dependencies first, then composes them:
const defaultAdvertiserWithHierarchy: AdvertiserWithHierarchyFragment = {
...createMockAdvertiser(),
hierarchy: createMockHierarchyWithParent(),
__typename: "Advertiser",
};You can customize generated factories and the tool will preserve your changes. When new fields are added to the fragment, they are automatically inserted while preserving your customizations.
Add a manual marker to prevent any automatic updates:
// @manual
const defaultUser: UserFragment = {
// This factory will never be auto-updated
};# advertisers.query.gql
query Advertisers($filter: String) {
advertisers(name: $filter) {
...Advertiser
}
}// advertisers.handler.ts
import { HttpResponse } from "msw";
import { fn } from "storybook/test";
import { createMockAdvertiser } from "../fragments/advertiser.factory";
import { mockAdvertisersQuery } from "./advertisers.query.generated";
export const advertisersSpy = fn();
export const advertisers = mockAdvertisersQuery(({ variables }) => {
advertisersSpy(variables);
return HttpResponse.json({
data: {
advertisers: [createMockAdvertiser()],
},
});
});
export default advertisers;For queries with nested wrapper types, the handler builds properly nested response objects:
export const summary = mockSummaryQuery(({ variables }) => {
summarySpy(variables);
return HttpResponse.json({
data: {
summary: {
metrics: [createMockPerformanceMetric()],
popMetrics: createMockPopMetrics(),
__typename: "SummaryResponse",
},
},
});
});The tool reads your project's package.json to determine the correct import path for fn():
- Projects with the
storybookpackage (v8+) useimport { fn } from "storybook/test" - Older projects use
import { fn } from "@storybook/test"
The tool creates a .gql-codegen-cache.json file to track generation history. Add this to your .gitignore:
.gql-codegen-cache.jsonCreate an src/gql/ids.ts file for consistent ID generation across factories:
export const ids = {
advertiser: [1, 2, 3],
hierarchy: [1, 2, 3],
file: ["1", "2", "3"],
};The tool automatically adds new entries to this file when it encounters id fields for new types.
The generated code expects these peer dependencies in your project:
{
"devDependencies": {
"@faker-js/faker": "^9.0.0",
"msw": "^2.0.0",
"storybook": "^8.0.0"
}
}For projects using Storybook 7, use @storybook/test instead of storybook.
Ensure your GraphQL fragments are saved with the .fragment.gql extension somewhere under src/.
This usually indicates a malformed factory file. Check that your factory follows the expected structure with a const defaultObjectName: TypeName = { ... }; pattern.
Check if your factory has manual modifications. The tool preserves manual changes by default. To force regeneration, delete the factory file and the .gql-codegen-cache.json cache file.
The cache file is gitignored, so on a fresh clone all auto-generated files will be regenerated on the next run. Files with manual markers (// @manual, // Custom) are left untouched.
- Fork the repository
- Create a feature branch
- Make your changes
- Submit a pull request
ISC License - see LICENSE file for details.