If you write GraphQL queries by hand and then define TypeScript types separately, you are maintaining two sources of truth that will inevitably drift apart. A renamed field in the schema, a new nullable return type, a deprecated argument -- any of these can silently break your frontend at runtime while the compiler stays quiet.
@graphql-codegen eliminates this problem entirely. It reads your GraphQL schema and operations, then generates TypeScript types, typed hooks, and even full SDK clients -- all at build time.
This guide walks through the full setup from zero to production-ready code generation.
Why Type Safety Matters for GraphQL
GraphQL already gives you a typed schema. The problem is that this type information lives on the server. Your TypeScript frontend has no compile-time knowledge of what query GetUser actually returns unless you manually write the corresponding interfaces.
Manual typing has three failure modes:
- Types go stale. The backend adds a field or changes a type. Your manually written interface stays the same. The bug shows up at runtime.
-
Types are incomplete. Developers skip optional fields or use
anyto save time. You lose the benefits of TypeScript. - Types are duplicated. The same query is used in three components, each with its own hand-rolled type definition.
Automated code generation solves all three. Types are derived directly from the schema and your .graphql files, generated fresh on every build, and shared across the project.
Setting Up @graphql-codegen
Install Dependencies
npm install -D @graphql-codegen/cli \
@graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-query
If you use Apollo Client instead of React Query, swap the last package:
npm install -D @graphql-codegen/typescript-react-apollo
Create the Config File
Create codegen.ts in your project root:
import type { 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',
'typescript-react-query',
],
config: {
reactQueryVersion: 5,
fetcher: {
func: './fetcher#useFetchData',
isReactHook: true,
},
enumsAsTypes: true,
skipTypename: false,
dedupeFragments: true,
},
},
},
hooks: {
afterAllFileWrite: ['prettier --write'],
},
};
export default config;
You can also use codegen.yml if you prefer YAML:
schema: "http://localhost:4000/graphql"
documents: "src/**/*.graphql"
generates:
./src/generated/graphql.ts:
plugins:
- typescript
- typescript-operations
- typescript-react-query
config:
reactQueryVersion: 5
enumsAsTypes: true
skipTypename: false
Add a Script
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
Run npm run codegen and the generator will introspect your schema, scan your .graphql files, and output a single graphql.ts file with everything typed.
Writing GraphQL Operations
Create your queries and mutations as .graphql files. This keeps them separate from component logic and makes them easy for the codegen to discover.
# src/graphql/queries/getUser.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
role
avatar
posts {
id
title
publishedAt
}
}
}
# src/graphql/mutations/updateUser.graphql
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
updateUser(id: $id, input: $input) {
id
name
email
role
}
}
What Gets Generated
After running npm run codegen, the generated file contains three layers of types.
1. Schema Types
These map directly to your GraphQL schema:
// Auto-generated -- do not edit
export type User = {
__typename?: 'User';
id: string;
name: string;
email: string;
role: UserRole;
avatar?: string | null;
posts: Array<Post>;
};
export type Post = {
__typename?: 'Post';
id: string;
title: string;
publishedAt: string;
};
export type UserRole = 'ADMIN' | 'EDITOR' | 'VIEWER';
export type UpdateUserInput = {
name?: string | null;
email?: string | null;
role?: UserRole | null;
};
2. Operation Types
These are scoped to each query or mutation you wrote:
export type GetUserQueryVariables = Exact<{
id: Scalars['ID']['input'];
}>;
export type GetUserQuery = {
__typename?: 'Query';
user: {
__typename?: 'User';
id: string;
name: string;
email: string;
role: UserRole;
avatar?: string | null;
posts: Array<{
__typename?: 'Post';
id: string;
title: string;
publishedAt: string;
}>;
};
};
3. Typed Hooks
With the React Query plugin, you get ready-to-use hooks:
export const useGetUserQuery = <TData = GetUserQuery, TError = unknown>(
variables: GetUserQueryVariables,
options?: Omit<UseQueryOptions<GetUserQuery, TError, TData>, 'queryKey'>
) =>
useQuery<GetUserQuery, TError, TData>({
queryKey: ['GetUser', variables],
queryFn: useFetchData<GetUserQuery, GetUserQueryVariables>(
GetUserDocument
).bind(null, variables),
...options,
});
Using Generated Code in Components
With the generated hooks, your components become clean and fully typed:
import { useGetUserQuery } from '@/generated/graphql';
function UserProfile({ userId }: { userId: string }) {
const { data, isLoading, error } = useGetUserQuery({ id: userId });
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error loading user</div>;
const user = data?.user;
if (!user) return <div>User not found</div>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<span>{user.role}</span>
<h2>Posts ({user.posts.length})</h2>
<ul>
{user.posts.map((post) => (
<li key={post.id}>
{post.title} - {post.publishedAt}
</li>
))}
</ul>
</div>
);
}
Everything is typed. user.name is a string. user.avatar is string | null. user.posts is an array of Post. If the backend renames publishedAt to createdAt, your next codegen run will produce a compile error exactly where you reference the old field.
Mutations work the same way:
import { useUpdateUserMutation } from '@/generated/graphql';
function EditUserForm({ userId }: { userId: string }) {
const { mutate, isPending } = useUpdateUserMutation();
const handleSubmit = (formData: FormData) => {
mutate({
id: userId,
input: {
name: formData.get('name') as string,
email: formData.get('email') as string,
},
});
};
return (
<form onSubmit={(e) => { e.preventDefault(); handleSubmit(new FormData(e.currentTarget)); }}>
<input name="name" placeholder="Name" />
<input name="email" placeholder="Email" />
<button type="submit" disabled={isPending}>
{isPending ? 'Saving...' : 'Save'}
</button>
</form>
);
}
The input parameter is typed as UpdateUserInput, so passing an unknown field like { username: '...' } will be caught at compile time.
Document Transforms
The @graphql-codegen pipeline supports document transforms that modify your operations before code generation. This is useful for injecting __typename fields, adding directives, or stripping client-only fields.
A common use case is adding the __typename field to every selection set for cache normalization:
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
import { addTypenameSelectionDocumentTransform } from '@graphql-codegen/client-preset';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.graphql'],
generates: {
'./src/generated/': {
preset: 'client',
documentTransforms: [addTypenameSelectionDocumentTransform],
config: {
enumsAsTypes: true,
},
},
},
};
export default config;
You can also write custom transforms to strip fields annotated with @client or rename operations for better cache key consistency.
Apollo Client Setup
If you use Apollo instead of React Query, switch the plugin:
generates: {
'./src/generated/graphql.ts': {
plugins: [
'typescript',
'typescript-operations',
'typescript-react-apollo',
],
config: {
withHooks: true,
withComponent: false,
withHOC: false,
},
},
},
This generates useGetUserQuery and useUpdateUserMutation hooks that wrap Apollo's useQuery and useMutation with full typing. The component API is identical -- you just swap the import source.
Custom Fetcher
The React Query plugin needs a fetcher function to actually execute GraphQL requests. Here is a minimal fetcher that works with any GraphQL endpoint:
// src/lib/fetcher.ts
export function useFetchData<TData, TVariables>(
query: string
): (variables?: TVariables) => Promise<TData> {
return async (variables?: TVariables) => {
const response = await fetch('http://localhost:4000/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${getToken()}`,
},
body: JSON.stringify({ query, variables }),
});
const json = await response.json();
if (json.errors) {
const message = json.errors
.map((e: { message: string }) => e.message)
.join('\n');
throw new Error(message);
}
return json.data as TData;
};
}
The fetcher config in codegen.ts points to this function. When the generated hooks call useFetchData, they pass the query document string and variables, and this function handles the HTTP layer. You can add authentication headers, error formatting, retry logic, or anything else your API requires.
For production apps, you typically read the endpoint URL from environment variables and add request interceptors for token refresh:
const API_URL = process.env.NEXT_PUBLIC_GRAPHQL_URL ?? 'http://localhost:4000/graphql';
Fragments for Reusable Type Selections
When multiple queries share the same selection of fields, use fragments to avoid duplication:
# src/graphql/fragments/userFields.graphql
fragment UserFields on User {
id
name
email
role
avatar
}
# src/graphql/queries/getUser.graphql
query GetUser($id: ID!) {
user(id: $id) {
...UserFields
posts {
id
title
publishedAt
}
}
}
The codegen recognizes fragments automatically. It generates a UserFieldsFragment type and inlines it into every query that references ...UserFields. This means changing the fragment in one place updates the types everywhere. Combined with the dedupeFragments: true config option, duplicate fragment definitions are merged, keeping the output file clean.
CI Integration
Add codegen to your CI pipeline to catch schema drift early:
# .github/workflows/codegen-check.yml
name: GraphQL Codegen Check
on: [pull_request]
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run codegen
- name: Check for uncommitted changes
run: |
git diff --exit-code src/generated/
echo "Generated types are up to date"
If a PR changes a .graphql file but forgets to run codegen, the diff check will fail.
Quick Conversions During Development
Sometimes you need to quickly convert a GraphQL schema to TypeScript interfaces for prototyping or documentation purposes, without setting up the full codegen pipeline. The free GraphQL to TypeScript converter handles this in the browser -- paste your schema and get TypeScript types immediately. It is useful for exploring an API schema before you commit to a full codegen setup.
For a deeper walkthrough of the codegen workflow with advanced configuration options, check the full GraphQL to TypeScript codegen guide.
Key Takeaways
- One source of truth. Your GraphQL schema defines the types. TypeScript consumes them. Nothing is manually maintained.
- Compile-time safety. Schema changes surface as TypeScript errors, not runtime crashes.
- Generated hooks. Whether you use React Query or Apollo, the codegen produces ready-to-use hooks with full type inference.
- Document transforms. Modify operations at build time for cache normalization, client field stripping, or custom conventions.
- CI enforcement. A simple diff check ensures generated code is never stale in your repository.
Stop writing GraphQL types by hand. Let the machine do it.
Top comments (0)