Introduction
GraphQL's type system is one of its greatest strengths, but without proper tooling, TypeScript developers often end up manually writing interface definitions that drift out of sync with the actual schema. GraphQL Code Generator solves this by automatically generating TypeScript types from your GraphQL schema and operations, creating a compile-time guarantee that your client code matches the server's API exactly.
In this comprehensive guide, we'll set up GraphQL Code Generator from scratch, explore its plugin architecture, implement patterns that scale from small projects to enterprise applications with hundreds of operations, and cover advanced techniques like fragment masking, custom scalars, and validation schema generation.
The Problem: Manual Type Definitions Are Fragile
Without code generation, TypeScript developers face a painful choice. They can write manual interfaces for every GraphQL response, which requires constant maintenance as the schema evolves. Or they can use any types and lose the benefits of TypeScript entirely. Both approaches lead to bugs—either stale types that don't match reality or no types at all.
Consider a typical GraphQL query without code generation:
// ❌ Manual type definition - must be updated every time the schema changes
interface GetUserQuery {
user: {
id: string;
name: string;
email: string;
avatar?: string;
role: string;
posts: Array<{
id: string;
title: string;
publishedAt: string;
}>;
};
}When the server adds a new field or renames an existing one, every manually written interface becomes stale. TypeScript won't catch the mismatch because the manual type is just an assertion—it's not derived from the actual schema. The result is runtime errors that could have been caught at compile time.
How GraphQL Code Generator Works
GraphQL Code Generator eliminates this tradeoff. It reads your schema, reads your operations, and produces TypeScript types that exactly match what the server will return. When a field is added to the schema, running codegen updates the types. When you rename a field in a query, the generated types change accordingly. TypeScript's compiler then catches any mismatches at build time.
Input Sources
Codegen requires two inputs: the schema and the operations. The schema can come from multiple sources:
- Introspection endpoint:
http://localhost:4000/graphql - Local SDL files:
./schema.graphqlor./schema/**/*.graphql - JSON introspection result:
./schema.json - Multiple sources: Array of URLs or files for federated schemas
Operations are found by scanning your source code for .graphql files or gql tagged template literals. The documents configuration option uses glob patterns to specify where to find them.
Output Artifacts
The generator produces several types of output depending on the plugins used:
// Base types (from typescript plugin)
export type User = {
__typename?: "User";
id: Scalars["ID"];
name: Scalars["String"];
email: Scalars["String"];
};
// Operation types (from typescript-operations plugin)
export type GetUserQuery = {
__typename?: "Query";
user: Pick<User, "id" | "name" | "email">;
};
// React hooks (from typescript-react-apollo plugin)
export function useGetUserQuery(
options?: Apollo.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(
GetUserDocument,
options
);
}Architecture and Design Patterns
Plugin Pipeline Architecture
Codegen processes your schema and operations through a plugin pipeline. Each plugin receives the output of the previous plugin and adds its own transformations. The typescript plugin generates base types, typescript-operations extends those with operation-specific types, and framework plugins add hooks or clients.
// Pipeline: schema → typescript → typescript-operations → typescript-react-apollo
// Each plugin adds more to the outputThis architecture allows you to mix and match plugins to generate exactly what your project needs. A Node.js backend might use typescript and typescript-graphql-request, while a React frontend uses typescript-react-apollo.
Preset System
Presets are pre-configured plugin combinations that generate output into multiple files. The client preset is the most common, generating a single output file with all types and a typed gql function:
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./schema.graphql",
documents: ["src/**/*.tsx"],
generates: {
"./src/gql/": {
preset: "client",
},
},
};The near-operation-file preset generates types next to each operation file, enabling better code organization in large projects.
Fragment Masking
Fragment masking is a pattern where components receive typed fragments rather than full query results. This creates clean component APIs where each component only sees the data it declared in its fragment:
// Component receives only its fragment's data
function UserAvatar({ user }: { user: FragmentType<typeof UserAvatarFragment> }) {
const data = useFragment(UserAvatarFragment, user);
return <img src={data.avatar} alt={data.name} />;
}Fragment masking encourages colocation—each component defines the data it needs, and the parent query aggregates fragments from all child components. This pattern scales elegantly as your component tree grows.
Step-by-Step Implementation
Initial Setup
Install the required packages and initialize the configuration:
# Install core packages
npm install -D @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo
# Or use the client preset (recommended for new projects)
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
# Generate initial config
npx graphql-codegen initWriting the Configuration
Create codegen.ts with your project-specific settings:
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
// Schema source
schema: "http://localhost:4000/graphql",
// Where to find operations
documents: ["src/**/*.{ts,tsx}", "!src/generated/**"],
// Output configuration
generates: {
"./src/generated/": {
preset: "client",
presetConfig: {
gqlTagName: "gql",
fragmentMasking: true,
},
},
},
// Format generated files
hooks: {
afterAllFileWrite: ["prettier --write"],
},
};
export default config;Defining Operations
Write your GraphQL operations in .graphql files or as tagged template literals. Each operation generates its own TypeScript type:
# src/operations/user.graphql
fragment UserFields on User {
id
name
email
avatar
role
}
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
posts {
id
title
publishedAt
}
}
}
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
...UserFields
}
}Running Generation
Execute codegen to generate types. In development, use watch mode for automatic regeneration:
# One-time generation
npx graphql-codegen
# Watch mode
npx graphql-codegen --watch
# Debug mode (shows detailed output)
npx graphql-codegen --config codegen.ts --verboseUsing Generated Types
Import and use the generated types in your components. TypeScript will enforce type safety across your entire API layer:
import { useGetUserQuery, useUpdateUserMutation } from "../generated/graphql";
import { graphql } from "../generated";
const GetUserDocument = graphql(`
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
}
}
`);
function UserProfile({ userId }: { userId: string }) {
const { data, loading, error } = useGetUserQuery({
variables: { id: userId },
});
if (loading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
const { user } = data;
return (
<div>
<img src={user.avatar} alt={user.name} />
<h1>{user.name}</h1>
<p>{user.email}</p>
<span>{user.role}</span>
<h2>Posts</h2>
{user.posts.map((post) => (
<article key={post.id}>
<h3>{post.title}</h3>
<time>{post.publishedAt}</time>
</article>
))}
</div>
);
}Framework-Specific Plugins
GraphQL Code Generator provides plugins for every major frontend framework. Each generates framework-specific hooks or components that integrate seamlessly with your application.
React with Apollo Client
// Generated hooks for React
export function useGetUsersQuery(
options?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>
) {
return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(
GetUsersDocument,
options
);
}
export function useCreateUserMutation(
options?: Apollo.MutationHookOptions<CreateUserMutation, CreateUserMutationVariables>
) {
return Apollo.useMutation<CreateUserMutation, CreateUserMutationVariables>(
CreateUserDocument,
options
);
}React with TanStack Query
For teams using TanStack Query instead of Apollo, the typescript-react-query plugin generates typed query functions:
import { useQuery } from "@tanstack/react-query";
import { useGetUsersQuery } from "./generated";
function UserList() {
const { data, isLoading, error } = useGetUsersQuery();
// data is fully typed as GetUsersQuery
}Vue with Apollo
The typescript-vue-apollo plugin generates Vue composition functions:
import { useGetUsersQuery } from "./generated";
export default defineComponent({
setup() {
const { result, loading, error } = useGetUsersQuery();
return { result, loading, error };
},
});Angular with Apollo
Angular developers get typed observable queries:
import { GetUsersGQL } from "./generated";
@Component({ /* ... */ })
class UserListComponent implements OnInit {
users: User[];
constructor(private getUsersGQL: GetUsersGQL) {}
ngOnInit() {
this.getUsersGQL.watch().valueChanges.subscribe(({ data }) => {
this.users = data.users;
});
}
}Svelte with Apollo
The typescript-svelte-apollo plugin generates Svelte-compatible reactive queries:
<script lang="ts">
import { GetUsers } from "./generated";
$: result = GetUsers();
</script>
{#each $result?.data?.users || [] as user}
<div>{user.name}</div>
{/each}Real-World Use Cases
Large-Scale SaaS Application
A SaaS platform with 200+ operations uses the near-operation-file preset to generate types alongside each feature module. Each team owns their feature's GraphQL operations, and codegen ensures cross-team type consistency. When the shared User type changes, every team's codegen run surfaces the exact components that need updating.
Mobile App with Shared Schema
A React Native app and web dashboard share the same GraphQL schema. Codegen runs once during the build process and generates types that both platforms consume. The shared type definitions eliminate the possibility of the mobile app sending different field selections than the web app expects.
Microservices with Federation
In a federated architecture, each microservice generates types from its own subgraph. A gateway service generates combined types from the supergraph. Codegen's federation support handles @key, @external, and @requires directives correctly, generating types that reflect the merged schema.
Validation Schema Generation
One powerful but lesser-known feature is generating validation schemas from your GraphQL input types. The typescript-validation-schema plugin generates Zod, Yup, or Valibot schemas that match your GraphQL inputs exactly:
// codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./schema.graphql",
generates: {
"./src/generated/graphql.ts": {
plugins: ["typescript"],
},
"./src/generated/validation.ts": {
plugins: [
{
"typescript-validation-schema": {
schema: "zod",
importFrom: "./graphql",
scalarSchemas: {
DateTime: "z.string().datetime()",
Email: "z.string().email()",
},
},
},
],
},
},
};This generates Zod schemas that validate form inputs before they reach your GraphQL mutations:
import { CreatePostInputSchema } from "./generated/validation";
// Validate form data against the GraphQL input type
const result = CreatePostInputSchema.safeParse(formData);
if (!result.success) {
console.error(result.error.issues);
}Best Practices for Production
-
Use the client preset for new projects: It provides the simplest API with automatic fragment masking and optimized bundle sizes.
-
Commit generated files: Include generated files in version control so all developers have consistent types without running codegen.
-
Add
--checkto CI: Runnpx graphql-codegen --checkin CI to verify generated files match the current schema and operations. -
Use fragment colocation: Place fragments in the same file as the component that uses them, or in a dedicated
fragments.graphqlfile per feature. -
Configure custom scalars: Map GraphQL scalars to appropriate TypeScript types to avoid losing type information:
config: {
scalars: {
DateTime: "string",
JSON: "Record<string, unknown>",
Upload: "File",
},
}-
Exclude generated files from linting: Add
src/generated/to your.eslintignoreto avoid linting auto-generated code. -
Use
enumsAsTypesfor better tree-shaking: Generate string literal union types instead of TypeScript enums. -
Set up IDE integration: Install the GraphQL extension for VS Code to get IntelliSense on
.graphqlfiles and inline operations.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Schema not reachable at build time | Codegen fails in CI | Use local schema files instead of introspection |
| Generated types too large | Slow TypeScript compilation | Split generation by domain |
| Circular fragment references | Codegen hangs or errors | Restructure fragments to avoid cycles |
Missing __typename in operations | Incomplete generated types | Add addTypename: true to client config |
| Enum values not matching server | Runtime errors | Use enumValues config for custom mapping |
Custom scalars defaulting to any | Loss of type safety | Always configure scalars mapping |
Performance Optimization
For large projects, codegen can become slow. Optimize by limiting the scope of documents globs, using the near-operation-file preset to split output, and running codegen in parallel for different schema sections:
// Split generation for faster builds
const config: CodegenConfig = {
generates: {
"./src/features/users/generated/": {
schema: "./schema.graphql",
documents: "src/features/users/**/*.graphql",
plugins: ["typescript", "typescript-operations"],
},
"./src/features/products/generated/": {
schema: "./schema.graphql",
documents: "src/features/products/**/*.graphql",
plugins: ["typescript", "typescript-operations"],
},
},
};Comparison with Alternatives
| Feature | GraphQL Codegen | tRPC | REST + Swagger Codegen |
|---|---|---|---|
| Type generation | Automatic | Inferred from TS | Automatic |
| Schema format | GraphQL SDL | TypeScript | OpenAPI YAML/JSON |
| Client hooks | Generated | Built-in | Manual |
| Multi-framework | Yes | Next.js focused | Limited |
| Fragment support | Yes | N/A | N/A |
| Plugin ecosystem | Rich | Limited | Moderate |
Advanced Patterns
Typed Fragments for Component APIs
Generate typed fragment references to create strongly-typed component props:
const UserAvatarFragment = graphql(`
fragment UserAvatar on User {
id
name
avatar
}
`);
function UserAvatar({ user }: { user: FragmentType<typeof UserAvatarFragment> }) {
const data = useFragment(UserAvatarFragment, user);
return <img src={data.avatar} alt={data.name} />;
}Custom Codegen Plugins
For project-specific needs, write custom plugins that generate additional artifacts:
// Custom plugin: generates mock factories
const plugin: CodegenPlugin = {
plugin: (schema, documents, config) => {
return documents.map((doc) => {
const operation = doc.operations[0];
return `
export const mock${operation.name} = (overrides?: Partial<${operation.name}Query>) => ({
...defaults,
...overrides,
});
`;
});
},
};Testing Generated Types
Generated types enable a testing strategy where type correctness is verified at compile time rather than runtime. Write tests that construct GraphQL response objects using the generated types, ensuring that your components handle all possible response shapes correctly:
import { graphql } from "./generated";
describe("Generated GraphQL types", () => {
it("generates correct query types", () => {
const document = graphql(`
query TestQuery {
users {
id
name
}
}
`);
expect(document.definitions).toHaveLength(1);
});
});Use the generated fragment types to create test fixtures that match the exact shape of real API responses. This eliminates the common testing problem where test data diverges from the actual API response shape over time. Generate mock data factories from your GraphQL schema that produce type-safe test data, ensuring that your mocks always match the current schema.
Future Outlook
GraphQL Code Generator is moving toward zero-config setups with automatic schema detection and intelligent plugin selection. The client preset already simplifies the developer experience significantly. Future developments include better support for server components, streaming subscriptions with @defer and @stream, and automatic resolver type generation.
The ecosystem continues to grow with community plugins for validation schemas, mock generation, and framework-specific integrations. As GraphQL adoption increases, code generation becomes an essential part of the TypeScript GraphQL stack.
Conclusion
GraphQL Code Generator is an essential tool for any TypeScript project using GraphQL. It eliminates manual type definitions, catches API mismatches at compile time, and generates framework-specific hooks that provide full IntelliSense support. The key takeaways are:
- Use the client preset for the simplest setup with fragment masking
- Commit generated files and verify them in CI with
--check - Use fragment colocation for scalable type management
- Configure custom scalars to preserve type information
- Split generation by domain for faster builds in large projects
- Generate validation schemas to validate inputs before they reach your API
The investment in setting up codegen pays off immediately by eliminating an entire category of bugs and dramatically improving the developer experience when working with GraphQL APIs.