Introduction
One of the most compelling advantages of GraphQL is its strongly-typed schema. Yet without proper tooling, TypeScript developers often find themselves manually typing API responses, creating a disconnect between the schema and the client code. GraphQL Code Generator bridges this gap by automatically generating TypeScript types, React hooks, and other artifacts directly from your GraphQL schema and operations.
This eliminates an entire class of bugs where your client code assumes a field exists or has a certain type, only to fail at runtime. With code generation, every query, mutation, and subscription produces fully-typed results that stay synchronized with your server schema.
Understanding Code Generation: Core Concepts
How GraphQL Code Generator Works
GraphQL Code Generator (codegen) operates by combining three inputs: your GraphQL schema, your operations (queries, mutations, subscriptions), and a configuration file. It parses all three, validates operations against the schema, and generates TypeScript (or other language) types as output.
The schema can be introspected from a running GraphQL server, loaded from local .graphql files, or extracted from a schema registry. Operations are typically .graphql files or template literals tagged with gql in your source code. The configuration file (codegen.ts or codegen.yml) specifies which plugins to use and how to generate the output.
The Plugin Architecture
Codegen's power comes from its plugin system. Each plugin generates a specific artifact:
typescript: Base TypeScript types for all schema typestypescript-operations: Types for your specific queries and mutationstypescript-react-apollo: Typed React hooks (useQuery,useMutation)typescript-graphql-request: Typed GraphQL client for Node.jstyped-document-node: Typed document nodes for framework-agnostic usage
You can combine multiple plugins to generate all the artifacts your application needs from a single configuration.
Schema-First vs. Code-First
GraphQL Code Generator works best with schema-first approaches where your schema is defined in .graphql SDL files. For code-first frameworks like TypeGraphQL or NestJS, codegen can still work by extracting the schema from the running server or generating SDL files as a build step.
Architecture and Design Patterns
The Generation Pipeline
The codegen pipeline follows a clear architecture: input collection β schema parsing β operation validation β type generation β file output. Each step is handled by plugins that transform the data in sequence.
// codegen.ts configuration
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: "src/**/*.graphql",
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript", "typescript-operations"],
},
"src/generated/hooks.tsx": {
plugins: ["typescript-react-apollo"],
config: {
withHooks: true,
withComponent: false,
withHOC: false,
},
},
},
};
export default config;Fragment Colocation Pattern
The fragment colocation pattern places GraphQL fragments alongside the components that use them. This creates a clear ownership model where each component declares exactly what data it needs:
# UserProfile.graphql
fragment UserBasicFields on User {
id
name
email
avatar
}
query GetUser($id: ID!) {
user(id: $id) {
...UserBasicFields
role
createdAt
}
}This pattern scales beautifully because components can import and compose fragments from other components, and codegen generates types for every fragment combination automatically.
The Generated Types Structure
Understanding the structure of generated types helps you work effectively with codegen output. For a typical schema, codegen generates:
// Base schema types
export type User = {
__typename?: "User";
id: Scalars["ID"];
name: Scalars["String"];
email: Scalars["String"];
posts: Array<Post>;
};
// Operation-specific types
export type GetUserQuery = {
__typename?: "Query";
user: {
__typename?: "User";
id: string;
name: string;
posts: Array<{
__typename?: "Post";
id: string;
title: string;
}>;
};
};
// Hook types
export function useGetUserQuery(
options: Apollo.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(
GetUserDocument,
options
);
}Step-by-Step Implementation
Installing Codegen Dependencies
Start by installing the required packages. The core CLI and plugins are all under the @graphql-codegen scope:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo
# Initialize configuration
npx graphql-codegen initConfiguring the Generator
Create a codegen.ts file at your project root. The configuration specifies where to find the schema, which files contain operations, and what artifacts to generate:
import { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./schema.graphql",
documents: ["src/**/*.{ts,tsx}", "!src/generated/**"],
generates: {
"./src/generated/": {
preset: "client",
plugins: [],
presetConfig: {
gqlTagName: "gql",
},
},
},
hooks: {
afterAllFileWrite: ["prettier --write"],
},
};
export default config;Writing Typed Operations
With codegen configured, write your GraphQL operations using the gql tag. The generated types will be inferred from these operations:
import { gql } from "./generated";
export const GET_USERS = gql(`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
id
name
email
role
}
}
`);
export const CREATE_USER = gql(`
mutation CreateUser($input: CreateUserInput!) {
createUser(input: $input) {
id
name
email
}
}
`);Using Generated Hooks
After running codegen, use the generated hooks in your React components. Every parameter and return value is fully typed:
import { useGetUsersQuery, useCreateUserMutation } from "./generated";
function UserList() {
const { data, loading, error } = useGetUsersQuery({
variables: { limit: 10, offset: 0 },
});
const [createUser] = useCreateUserMutation({
refetchQueries: [{ query: GetUsersDocument }],
});
if (loading) return <Spinner />;
if (error) return <ErrorDisplay error={error} />;
return (
<div>
{data.users.map((user) => (
<div key={user.id}>
<h3>{user.name}</h3>
<p>{user.email}</p>
<span>{user.role}</span>
</div>
))}
</div>
);
}Running Code Generation
Execute codegen with the CLI. During development, watch mode regenerates types whenever your schema or operations change:
# One-time generation
npx graphql-codegen
# Watch mode for development
npx graphql-codegen --watch
# With specific config file
npx graphql-codegen --config codegen.tsReal-World Use Cases
E-Commerce Platform
An e-commerce platform uses codegen to generate types for product catalogs, shopping carts, and checkout flows. Each domain has its own set of .graphql files colocated with the components that use them. When the product team adds a new field to the Product type, all client code automatically gets the new type, and TypeScript errors show exactly which components need updating.
SaaS Dashboard
A SaaS application with complex reporting features generates typed hooks for each report type. The generated useDashboardMetricsQuery hook returns fully typed metric objects, making it trivial to build chart components that consume the data without any manual type assertions.
Multi-Platform Mobile App
A React Native app shares its GraphQL operations with a web frontend. Codegen generates both React Apollo hooks for the web and generic typed document nodes that work with any GraphQL client, enabling code sharing across platforms while maintaining full type safety.
Best Practices for Production
-
Commit generated files to version control: This ensures all developers have the latest types and eliminates a build step during development.
-
Run codegen in CI with
--checkmode: Addnpx graphql-codegen --checkto your CI pipeline to verify that generated files are up to date. -
Use fragment colocation: Place fragments next to components for better code organization and automatic tree-shaking of unused types.
-
Configure
gqlTagNamefor optimal tree-shaking: Usegqlfrom the generated module instead of@apollo/clientto enable better dead code elimination. -
Set up
afterAllFileWritehooks: Run Prettier or ESLint on generated files to maintain consistent formatting. -
Use
preset: "client"for modern setups: The client preset simplifies configuration and generates a more compact, modern API. -
Separate schema and operations generation: Generate base types and operation types in separate outputs for cleaner imports.
-
Document your codegen configuration: Add comments explaining plugin choices and configuration options for team members.
Common Pitfalls and Solutions
| Pitfall | Impact | Solution |
|---|---|---|
| Generated files out of sync | Runtime type mismatches | Add --check to CI, use watch mode locally |
| Fragment spread depth errors | Codegen fails with cryptic errors | Check for circular fragment references |
| Enum naming conflicts | TypeScript compilation errors | Configure enumValues in codegen config |
| Large generated files | Slow IDE performance | Split generation by domain/feature |
Missing __typename | Normalization breaks | Always include __typename in operations |
Performance Optimization
Codegen performance matters in large projects with hundreds of operations. Use the documents glob to exclude test files and node_modules, configure incremental generation to only regenerate changed files, and run codegen as a background process during development.
// Optimize codegen config for large projects
const config: CodegenConfig = {
schema: "./schema.graphql",
documents: [
"src/features/**/operations.graphql",
"!src/**/*.test.ts",
"!src/**/*.spec.ts",
],
generates: {
"./src/generated/types.ts": {
plugins: ["typescript", "typescript-operations"],
config: {
skipTypename: true, // Reduces generated type size
enumsAsTypes: true, // Better tree-shaking
},
},
},
};Comparison with Alternatives
| Feature | GraphQL Codegen | tRPC | REST + OpenAPI |
|---|---|---|---|
| Schema source | GraphQL SDL | TypeScript types | OpenAPI spec |
| Type generation | Automatic | From TypeScript | Codegen needed |
| Client hooks | Generated | Built-in | Manual |
| Multi-language | Yes | TypeScript only | Multiple |
| Learning curve | Moderate | Low | Low |
| Ecosystem maturity | Very mature | Growing | Very mature |
Advanced Patterns
Custom Scalars
Define custom scalar types to map GraphQL scalars to TypeScript types:
const config: CodegenConfig = {
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript"],
config: {
scalars: {
DateTime: "Date",
JSON: "Record<string, unknown>",
Email: "string",
},
},
},
},
};Schema Stitching Support
For federated or stitched schemas, codegen can generate types that span multiple services. Use the @graphql-codegen/schema-ast plugin to extract schemas from each service and combine them:
const config: CodegenConfig = {
schema: [
"http://localhost:4001/graphql",
"http://localhost:4002/graphql",
],
generates: {
"./src/generated/combined.ts": {
plugins: ["typescript", "typescript-operations"],
},
},
};Typed Document Nodes for Framework-Agnostic Usage
The typed-document-node plugin generates framework-agnostic typed document nodes that work with any GraphQL client. This is particularly useful when sharing operations between React, Vue, Svelte, or vanilla JavaScript:
import { TypedDocumentNode } from "@graphql-codegen/typed-document-node";
// Generated typed document node
export const GetUserDocument: TypedDocumentNode<
GetUserQuery,
GetUserQueryVariables
> = {
kind: Kind.DOCUMENT,
definitions: [
/* ... */
],
};
// Works with any GraphQL client
const result = await client.request(GetUserDocument, { id: "1" });
// result is fully typed as GetUserQueryPersisted Queries Generation
Generate persisted query manifests to reduce network overhead and improve security. The manifest maps query hashes to query strings, allowing the server to reject arbitrary queries:
const config: CodegenConfig = {
generates: {
"./src/generated/persisted-documents.json": {
plugins: ["graphql-codegen-persisted-query-ids"],
config: {
output: "apollo",
algorithm: "sha256",
},
},
},
};With persisted queries enabled, the client sends only the query hash instead of the full query string. This reduces request payload size by 60-80% for complex queries and prevents attackers from executing arbitrary GraphQL operations against your API.
CI/CD Integration with GitHub Actions
Automate codegen validation in your CI pipeline to catch schema drift before it reaches production:
# .github/workflows/graphql-codegen.yml
name: GraphQL Codegen Check
on: [push, pull_request]
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Generate types
run: npx graphql-codegen
- name: Check for uncommitted changes
run: |
if [ -n "$(git status --porcelain src/generated/)" ]; then
echo "Generated types are out of date. Run 'npx graphql-codegen' and commit the changes."
git diff src/generated/
exit 1
fiThis workflow runs on every push and pull request, ensuring that generated types are always in sync with the schema. If a developer forgets to regenerate types after changing the schema, the CI check fails and provides clear instructions on how to fix it.
Testing Strategies
Test your codegen configuration by verifying that generated types match your expectations. Create snapshot tests that compare generated output to expected baselines:
import { readFileSync } from "fs";
describe("GraphQL Codegen", () => {
it("generates correct types for User query", () => {
const generated = readFileSync("src/generated/graphql.ts", "utf-8");
expect(generated).toContain("export type GetUserQuery");
expect(generated).toContain("user: {");
});
});Vue.js and Angular Integration
GraphQL Code Generator supports multiple frontend frameworks through dedicated plugins. For Vue.js applications using Apollo Vue, generate typed composables:
// codegen.ts for Vue
const config: CodegenConfig = {
generates: {
"./src/generated/graphql.ts": {
plugins: [
"typescript",
"typescript-operations",
"typescript-vue-apollo",
],
config: {
withCompositionFunctions: true,
vueApolloComposableImportFrom: "@vue/apollo-composable",
},
},
},
};
// Usage in Vue component
import { useGetUsersQuery } from "@/generated/graphql";
export default defineComponent({
setup() {
const { result, loading, error } = useGetUsersQuery({
limit: 10,
});
return { users: computed(() => result.value?.users ?? []), loading, error };
},
});For Angular applications using Apollo Angular, generate typed services:
// codegen.ts for Angular
const config: CodegenConfig = {
generates: {
"./src/generated/graphql.ts": {
plugins: [
"typescript",
"typescript-operations",
"typescript-apollo-angular",
],
},
},
};
// Usage in Angular component
@Injectable({ providedIn: "root" })
export class UserService {
constructor(private getUsersGQL: GetUsersGQL) {}
getUsers(limit: number) {
return this.getUsersGQL.watch({ limit }).valueChanges.pipe(
map((result) => result.data.users)
);
}
}Handling Unions and Interfaces
GraphQL unions and interfaces generate discriminated union types in TypeScript, enabling exhaustive pattern matching:
union SearchResult = User | Post | Comment
interface Node {
id: ID!
}
type User implements Node {
id: ID!
name: String!
email: String!
}
type Post implements Node {
id: ID!
title: String!
content: String!
}// Generated discriminated union
export type SearchResult =
| { __typename: "User"; id: string; name: string; email: string }
| { __typename: "Post"; id: string; title: string; content: string }
| { __typename: "Comment"; id: string; text: string };
// Exhaustive switch with TypeScript
function renderResult(result: SearchResult) {
switch (result.__typename) {
case "User":
return <UserCard name={result.name} email={result.email} />;
case "Post":
return <PostCard title={result.title} content={result.content} />;
case "Comment":
return <CommentCard text={result.text} />;
default:
// TypeScript ensures all cases are handled
const _exhaustive: never = result;
return null;
}
}This pattern catches missing cases at compile time. If a new type is added to the union, TypeScript flags every switch statement that doesn't handle it.
Future Outlook
The GraphQL code generation ecosystem is evolving toward zero-configuration setups where types are generated automatically based on your project structure. The client preset already simplifies this significantly. Future developments include better support for server components, streaming operations, and automatic fragment composition.
The rise of GraphQL federation and schema composition tools like Apollo Federation and Hive is driving codegen toward unified type generation across distributed architectures. Instead of generating types per service, future tools will generate a single unified type system that spans all subgraphs, with automatic resolution of shared types and cross-service references. This eliminates the manual coordination required when services share types today.
Integration with AI-powered development tools is also emerging. Codegen plugins that generate documentation, suggest query optimizations, and automatically fix schema violations based on usage patterns are in development. These tools will analyze your codebase to identify unused fields, suggest fragment consolidation, and recommend schema improvements based on how your client code actually uses the API.
Performance Optimization with Code Generation
Code generation can also optimize GraphQL performance. Generate persisted query manifests that map query strings to hash IDs, allowing the server to reject unknown queries and reduce bandwidth by sending hashes instead of full query strings. Generate DataLoader factories from your schema that automatically batch and cache database queries, solving the N+1 problem that plagues naive GraphQL implementations. Generate response type validators that verify server responses match the expected schema at runtime, catching server-side bugs that would otherwise manifest as undefined behavior in the client. These generated utilities combine type safety with runtime safety, ensuring that your application handles unexpected data gracefully.
Migration and Adoption Strategy
Adopting GraphQL code generation in an existing project requires a phased approach. Start by adding code generation for your most critical queries, those used in your most important user flows. Generate types for these queries first and update the consuming components to use the generated types. This incremental approach lets you experience the benefits without a large upfront investment. As your team becomes comfortable with the generated types, expand code generation to cover all queries and mutations. Remove manual type definitions that are now redundant. Update your development workflow to regenerate types automatically when the schema changes, using file watchers or pre-commit hooks. Document the code generation setup so new team members can understand the workflow. The migration typically takes one to two sprints for a medium-sized codebase and pays for itself through reduced type-related bugs and faster feature development.
Conclusion
GraphQL Code Generator transforms how TypeScript developers work with GraphQL by eliminating manual type definitions and ensuring compile-time safety across your entire API layer. The key takeaways are:
- Set up codegen early in your projectβit pays dividends immediately
- Use fragment colocation for scalable type management
- Integrate codegen into your CI pipeline with
--checkmode - Leverage the plugin system to generate exactly the artifacts you need
- Use watch mode during development for instant type updates
Start with the client preset for the simplest setup, then customize as your needs grow. Once your team experiences fully-typed GraphQL operations, going back to manually typing API responses becomes unthinkable.