Why Code Generation?
One of GraphQL's greatest strengths is its strong type system, but without code generation, you end up manually writing TypeScript types that mirror your schema — a tedious, error-prone process that falls out of sync. GraphQL Code Generator automatically generates TypeScript types, hooks, and utilities from your schema and queries.
What Code Generation Provides
- TypeScript types from schema: Generate interfaces for all your GraphQL types automatically
- Typed operations: Generate exact types for each query and mutation result
- Typed React hooks: Generate custom hooks like
useGetUserQuerywith full type safety - Resolver types: Generate resolver type signatures for the server
- Compile-time validation: Catch type errors at build time, not runtime
Setup
# Install GraphQL Code Generator and plugins
npm install -D @graphql-codegen/cli
npm install -D @graphql-codegen/typescript
npm install -D @graphql-codegen/typescript-operations
npm install -D @graphql-codegen/typescript-react-apollo
npm install -D @graphql-codegen/typescript-resolvers
# Initialize configuration
npx graphql-code-generator init
Configuration File
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
// Schema source — can be URL, file, or glob
schema: 'http://localhost:4000/graphql',
// Document source — your .graphql files or inline gql tags
documents: ['src/**/*.graphql', 'src/**/*.tsx'],
generates: {
// Client-side types and hooks
'src/generated/graphql.ts': {
plugins: [
'typescript', // Base TypeScript types
'typescript-operations', // Types for queries/mutations
'typescript-react-apollo', // React hooks
],
config: {
withHooks: true,
withHOC: false,
withComponent: false,
scalars: {
DateTime: 'string',
JSON: 'Record<string, unknown>',
},
enumsAsTypes: true,
avoidOptionals: {
field: true,
inputValue: false,
object: true,
},
},
},
// Server-side resolver types
'src/generated/resolvers-types.ts': {
plugins: ['typescript', 'typescript-resolvers'],
config: {
contextType: '../context#Context',
mapperTypeSuffix: 'Model',
mappers: {
User: '../models#UserModel',
Post: '../models#PostModel',
},
},
},
},
// Ignore patterns
ignoreNoDocuments: true,
};
export default config;
Writing GraphQL Operations
# src/graphql/queries.graphql
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
bio
avatarUrl
posts {
id
title
publishedAt
}
}
}
query GetPosts($limit: Int!, $offset: Int, $filter: PostFilterInput) {
posts(limit: $limit, offset: $offset, filter: $filter) {
id
title
excerpt
slug
isPublished
author {
id
name
avatarUrl
}
publishedAt
likesCount
}
}
# src/graphql/mutations.graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
slug
isPublished
createdAt
}
}
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
id
title
body
isPublished
updatedAt
}
}
mutation DeletePost($id: ID!) {
deletePost(id: $id)
}
Running Code Generation
# Generate types
npx graphql-codegen
# Watch mode for development
npx graphql-codegen --watch
# Add to package.json scripts
# "codegen": "graphql-codegen",
# "codegen:watch": "graphql-codegen --watch"
Using Generated Types in React
// The generated file exports typed hooks and types
// src/generated/graphql.ts (auto-generated)
// export type User = { id: string; name: string; email: string; ... }
// export type GetUserQuery = { user: { id: string; name: string; ... } | null }
// export type GetUserQueryVariables = { id: string }
// export function useGetUserQuery(options: ...) { ... }
// Using generated hooks in components
import {
useGetUserQuery,
useGetPostsQuery,
useCreatePostMutation,
GetPostsQueryVariables,
PostFilterInput,
} from '@/generated/graphql';
function UserProfile({ userId }: { userId: string }) {
// Fully typed — data, loading, error are all correctly typed
const { data, loading, error } = useGetUserQuery({
variables: { id: userId }, // TypeScript ensures id is a string
});
if (loading) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
// data.user is typed — autocomplete works on all fields
const user = data?.user;
if (!user) return <p>User not found</p>;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<p>{user.bio}</p>
{user.posts.map(post => (
<div key={post.id}>{post.title}</div>
))}
</div>
);
}
function PostListPage() {
const [filter, setFilter] = useState<PostFilterInput>({});
const { data, loading, fetchMore } = useGetPostsQuery({
variables: { limit: 10, offset: 0, filter },
});
return (
<div>
{data?.posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>By {post.author.name}</p>
<span>{post.likesCount} likes</span>
</article>
))}
</div>
);
}
function CreatePostForm() {
const [createPost, { loading }] = useCreatePostMutation({
// TypeScript ensures the input matches CreatePostInput
onCompleted: (data) => {
// data.createPost is fully typed
console.log('Created:', data.createPost.title);
},
});
const handleSubmit = (title: string, body: string) => {
createPost({
variables: {
input: { title, body, isPublished: true },
},
});
};
// ...
}
Server-Side Resolver Types
// Using generated resolver types on the server
import { Resolvers } from './generated/resolvers-types';
// Full type safety for all resolvers
const resolvers: Resolvers = {
Query: {
// TypeScript enforces correct argument types and return types
user: async (_parent, args, ctx) => {
// args.id is typed as string
return ctx.prisma.user.findUnique({ where: { id: args.id } });
},
posts: async (_parent, args, ctx) => {
// args.limit, args.offset, args.filter are all correctly typed
return ctx.prisma.post.findMany({
take: args.limit ?? 20,
skip: args.offset ?? 0,
});
},
},
User: {
// parent is typed as UserModel (from mappers config)
posts: async (parent, _args, ctx) => {
return ctx.prisma.post.findMany({
where: { authorId: parent.id },
});
},
},
Mutation: {
createPost: async (_parent, args, ctx) => {
// args.input is typed as CreatePostInput
return ctx.prisma.post.create({
data: {
title: args.input.title,
body: args.input.body,
authorId: ctx.currentUser!.id,
},
});
},
},
};
Code Generation Best Practices
- Run in CI: Add codegen to your CI pipeline to ensure generated types are always up to date
- Commit generated files: Check generated files into version control so they are available without running codegen
- Watch mode in dev: Run codegen in watch mode during development for instant type updates
- Use mappers: Map GraphQL types to your database models on the server side
- Separate client/server output: Generate client types and server resolver types in separate files