configuring types, hooks and fragments properly
The default codegen.yml from the quickstart barely covers half of what GraphQL Code Generator can actually do. Custom scalars, fragment masking and the near-operation-file preset decide whether generated code is a joy or a burden.
Table of Contents
- 1. Why the codegen.yml deserves more attention
- 2. Basic structure of a codegen.yml: schema, documents, generates
- 3. Plugins in detail: typescript, typescript-operations, typescript-react-apollo
- 4. Named operations vs. the near-operation-file preset
- 5. Mapping custom scalars correctly
- 6. Fragment masking for clean component boundaries
- 7. Configuring hook generation: withHooks, withHOC, withComponent
- 8. Watch mode, caching and performance on large schemas
- 9. GraphQL Code Generator presets compared
- 10. Summary
- 11. FAQ
1. Why the codegen.yml deserves more attention
Most teams set up GraphQL Code Generator once through the init wizard and never touch the codegen.yml again, even though this exact file decides whether the generated code saves time day to day or creates extra friction. A default configuration without fragment masking produces types that give every component access to the entire query result, instead of just the fields the component actually requested, undoing GraphQL's core strength, colocated data requirements, right back inside the generated code.
A well thought-out GraphQL Code Generator config pays off especially in growing codebases: custom scalars that correctly type DateTime as string instead of any, the near-operation-file preset that places types next to their matching query file instead of a monolithic generated.ts, and fragment masking that makes component boundaries visible in the type system. This article covers all three in depth, beyond what the official quickstart guide shows.
2. Basic structure of a codegen.yml: schema, documents, generates
Every codegen.yml consists of three core blocks: schema defines the source of the GraphQL schema, either as a local SDL file, an introspection endpoint, or a combination of both for different environments. documents defines which files are scanned for GraphQL operations, usually a glob pattern like src/**/*.graphql or src/**/*.tsx for inline queries in template literals. generates is the actual output configuration: for every target file or directory, it defines which plugins run and in what order.
The most common beginner mistake in the GraphQL Code Generator config is a documents pattern that's too broad and accidentally re-reads previously generated files, producing growing, self-duplicating output on every run. An explicit !src/generated/** exclusion in the glob pattern reliably prevents this infinite loop and should be present in every configuration that keeps generated files in the same directory tree.
# codegen.yml - base configuration
schema: "https://api.mironsoft.dev/graphql"
documents:
- "src/**/*.graphql"
- "src/**/*.tsx"
- "!src/generated/**"
generates:
src/generated/graphql.ts:
plugins:
- "typescript"
- "typescript-operations"
- "typescript-react-apollo"
config:
withHooks: true
3. Plugins in detail: typescript, typescript-operations, typescript-react-apollo
GraphQL Code Generator is deliberately built as a plugin pipeline, not a monolithic tool. The typescript plugin generates the base types directly from the schema, meaning interfaces for every type, input, and enum. The typescript-operations plugin builds on top of it and generates specific types for every individual query, mutation and fragment, tailored exactly to the fields actually requested rather than the full type.
The typescript-react-apollo plugin requires both previous plugins and generates React hooks like useProductQuery and useCreateProductMutation, fully typed for both variables and return value. This plugin chain is deliberately modular: teams using urql instead of Apollo Client simply swap typescript-react-apollo for typescript-urql, while typescript and typescript-operations stay unchanged since they're client-agnostic.
4. Named operations vs. the near-operation-file preset
The default configuration bundles all generated types into a single, often thousands-of-lines-long file. Past fifty queries or so, this file becomes unwieldy, and every small schema change triggers a merge conflict in that exact one file, even when two developers are working on completely unrelated features. The near-operation-file preset solves this by generating a dedicated .generated.ts file directly next to every .graphql file.
This colocation mirrors GraphQL's core principle exactly: a component defines its data requirement locally, and the matching types live just as locally instead of in one central file shared and managed across all teams. Merge conflicts in generated files are almost entirely eliminated as a result, since different features almost never touch the same .generated.ts file.
# codegen.yml - near-operation-file preset for colocation
generates:
src/:
preset: "near-operation-file"
presetConfig:
extension: ".generated.ts"
baseTypesPath: "generated/graphql.ts"
plugins:
- "typescript-operations"
- "typescript-react-apollo"
5. Mapping custom scalars correctly
Without explicit configuration, GraphQL Code Generator maps every custom scalar, for example DateTime, Decimal, or JSON, to any by default. That's the fastest way to lose TypeScript's type safety on exactly the fields that are most error-prone in practice: date calculations and monetary amounts. The scalars block in the configuration lets you map every custom scalar to a concrete TypeScript type.
For DateTime, string is usually more precise than Date, because GraphQL scalars are always serialized as strings over the wire anyway, and the conversion to a Date object should happen explicitly in application code, not implicitly in the generated type. For Decimal fields, which Magento GraphQL uses for prices for instance, string is also the safer choice over number, since JavaScript's floating-point arithmetic can introduce rounding errors on monetary amounts.
# codegen.yml - explicit scalar mapping instead of implicit "any"
config:
scalars:
DateTime: "string"
Decimal: "string"
JSON: "Record<string, unknown>"
Upload: "File"
6. Fragment masking for clean component boundaries
Without fragment masking, every component embedding a fragment has full TypeScript access to all fields of that fragment, regardless of whether the component actually uses them. That undermines the idea that a fragment is a component's private data requirement. Fragment masking, enabled through the client-preset, instead produces an opaque FragmentType that only the useFragment helper of that exact fragment can resolve.
The practical effect: a parent component can pass a fragment down to a child component without being able to access its fields itself. If the child component later changes which fields it needs, only its own fragment needs updating, the parent component stays unchanged and keeps compiling without errors, as long as it only passes the fragment through instead of using its contents directly.
// ProductCard.tsx — fragment owns its own field selection
import { graphql, useFragment } from '../generated';
const ProductCardFragment = graphql(`
fragment ProductCard on Product {
id
name
price
}
`);
export function ProductCard(props: { product: FragmentType<typeof ProductCardFragment> }) {
// useFragment "unmasks" the opaque type, only inside this component
const product = useFragment(ProductCardFragment, props.product);
return <div>{product.name} — {product.price}</div>;
}
7. Configuring hook generation: withHooks, withHOC, withComponent
The typescript-react-apollo plugin offers three distinct integration styles, toggled via configuration flags: withHooks generates functions like useProductQuery, by far the most common style in modern React codebases today. withHOC instead generates higher-order components in the style of older class-component architectures, and withComponent generates render-prop components, a pattern that was common before React hooks existed.
In new projects, only withHooks: true should be enabled, with the other two options explicitly disabled, since every additional output variant increases the generated file's size without adding value unless legacy class components in the same codebase actually depend on HOCs. Migration projects moving gradually from class components to hooks, on the other hand, benefit from generating both variants simultaneously during the transition period.
8. Watch mode, caching and performance on large schemas
On schemas with several thousand fields, a full codegen run becomes noticeably slow, especially in --watch mode during development. The overwrite: true option combined with an incremental watch mode that only reprocesses changed documents reduces the latency between saving a .graphql file and having the generated code available to under a second in most setups.
For CI pipelines, a separate, non-incremental run with --check is worth adding, which verifies whether the checked-in generated files still match the current schema and current queries, without actually overwriting the files. A pull request that changes the schema or queries but hasn't updated the generated files reliably fails as a result, instead of silently letting stale types slip into the main branch.
{
"scripts": {
"codegen": "graphql-codegen --config codegen.yml",
"codegen:watch": "graphql-codegen --config codegen.yml --watch",
"codegen:check": "graphql-codegen --config codegen.yml --check"
}
}
9. GraphQL Code Generator presets compared
Choosing the right preset has a bigger impact on developer experience than any single plugin option. The table below compares the three common approaches.
| Approach | Output | Advantage | Drawback |
|---|---|---|---|
| Single-file (default) | One large generated.ts | Simple setup, a single import path | Merge conflicts, unwieldy past ~50 queries |
| near-operation-file | .generated.ts next to every .graphql file | Colocation, almost no merge conflicts | More files in the project tree |
| client-preset (fragment masking) | Central generated directory with graphql() function | Enforces clean component boundaries | Steeper learning curve, more boilerplate per component |
In practice, near-operation-file and the client-preset can be combined: colocation for file structure, fragment masking for type safety at component boundaries. This combination is now the default configuration recommended by the GraphQL Code Generator documentation itself for new React projects.
Mironsoft
GraphQL tooling, TypeScript codegen and React frontend architecture
Generated types that actually help instead of just existing?
We configure GraphQL Code Generator for your project, including fragment masking, custom scalars and colocation, and migrate existing single-file setups gradually without breaking the build.
Codegen setup
Configure codegen.yml, presets and plugin selection for your frontend
Fragment masking
Make component boundaries visible and enforceable in the type system
CI safeguards
codegen --check as a pull request gate against stale generated types
10. Summary
A well thought-out GraphQL Code Generator config differs from the quickstart configuration in three decisive points: the near-operation-file preset for colocation instead of a monolithic output file, explicit custom scalar mapping instead of silent any, and fragment masking for component boundaries that are actually enforced in the type system. Each of these three decisions directly affects maintainability and how often merge conflicts occur.
The plugin pipeline of typescript, typescript-operations and typescript-react-apollo stays client-agnostic and swappable, and --check in the CI pipeline ensures generated types never lag behind the actual schema. Configure these building blocks correctly once, and you gain type safety that actually fits your daily workflow instead of slowing it down.
GraphQL Code Generator Config — The Essentials at a Glance
Colocation
The near-operation-file preset places generated types next to their matching query file, not in one monolithic file.
Custom scalars
Map DateTime, Decimal and JSON explicitly in the scalars block instead of letting them fall back to any.
Fragment masking
client-preset with useFragment enforces that components only see their own requested fields.
CI safeguards
graphql-codegen --check prevents stale generated types from landing on the main branch.