GraphQL’s type system is valuable — but only if your client code reflects it. Without code generation, you’re writing TypeScript types by hand that drift from the schema, or using any and losing all type safety. graphql-codegen generates TypeScript types, React hooks, and document types directly from your schema and queries. Claude Code handles the configuration that makes codegen actually useful in a real project.
This guide covers GraphQL code generation with Claude Code: codegen setup, typed hooks, fragment patterns, and schema-first development.
graphql-codegen Setup
Set up graphql-codegen to generate TypeScript types and
Apollo Client hooks from our GraphQL schema and queries.
# codegen.ts (or codegen.yml)
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql', # Or path to local schema.graphql
documents: ['src/**/*.graphql', 'src/**/*.{ts,tsx}'],
generates: {
# 1. Base TypeScript types from schema
'src/generated/graphql.ts': {
plugins: ['typescript', 'typescript-operations'],
config: {
scalars: {
DateTime: 'string',
UUID: 'string',
JSON: 'Record<string, unknown>',
},
enumsAsTypes: true,
avoidOptionals: true, # Use null instead of undefined for nullable fields
},
},
# 2. Apollo Client typed hooks
'src/generated/graphql-hooks.ts': {
preset: 'import-types',
presetConfig: {
typesPath: './graphql',
},
plugins: ['typescript-react-apollo'],
config: {
withHooks: true,
withResultType: true,
withMutationOptionsType: true,
},
},
# 3. Per-file fragments (introspection)
'src/generated/fragment-matcher.ts': {
plugins: ['fragment-matcher'],
},
},
hooks: {
afterAllFileWrite: ['prettier --write'],
},
};
export default config;
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
Defining Queries with Fragments
Set up a pattern where each component declares the data it needs
using fragments, and the parent composes them into full queries.
Component Fragments
# src/components/UserCard/UserCard.graphql
fragment UserCard_user on User {
id
name
email
avatarUrl
joinedAt
}
# src/components/PostCard/PostCard.graphql
fragment PostCard_post on Post {
id
title
excerpt
publishedAt
author {
...UserCard_user
}
}
# src/pages/BlogPage/BlogPage.graphql
query BlogPage($page: Int! = 1, $limit: Int! = 20) {
posts(page: $page, limit: $limit) {
items {
...PostCard_post
}
pagination {
currentPage
totalPages
totalItems
}
}
}
After running npm run codegen, TypeScript types are generated automatically:
// Auto-generated — src/generated/graphql.ts (excerpt)
export type UserCard_UserFragment = {
id: string;
name: string;
email: string;
avatarUrl: string | null;
joinedAt: string;
};
export type PostCard_PostFragment = {
id: string;
title: string;
excerpt: string;
publishedAt: string;
author: UserCard_UserFragment;
};
export type BlogPageQueryVariables = Exact<{
page: Scalars['Int']['input'];
limit: Scalars['Int']['input'];
}>;
export type BlogPageQuery = {
posts: {
items: Array<PostCard_PostFragment>;
pagination: {
currentPage: number;
totalPages: number;
totalItems: number;
};
};
};
Using Generated Hooks
// src/pages/BlogPage.tsx
import { useBlogPageQuery } from '../generated/graphql-hooks';
import { PostCard } from '../components/PostCard';
export function BlogPage() {
const [page, setPage] = useState(1);
// useBlogPageQuery is generated — fully typed, matches the query exactly
const { data, loading, error } = useBlogPageQuery({
variables: { page, limit: 20 },
notifyOnNetworkStatusChange: true,
});
if (loading && !data) return <Skeleton />;
if (error) return <ErrorMessage error={error} />;
return (
<div>
{data?.posts.items.map(post => (
// PostCard receives PostCard_PostFragment shape — enforced by TypeScript
<PostCard key={post.id} post={post} />
))}
<Pagination
current={data?.posts.pagination.currentPage ?? 1}
total={data?.posts.pagination.totalPages ?? 1}
onChange={setPage}
/>
</div>
);
}
// src/components/PostCard.tsx
import type { PostCard_PostFragment } from '../../generated/graphql';
interface Props {
post: PostCard_PostFragment; // Type comes from codegen
}
export function PostCard({ post }: Props) {
return (
<article>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
<UserCard user={post.author} /> {/* UserCard type also enforced */}
<time>{new Date(post.publishedAt).toLocaleDateString()}</time>
</article>
);
}
The fragment on PostCard_PostFragment is the source of truth. If someone removes a field from the GraphQL schema, TypeScript errors appear immediately — at compile time, before the code ships.
Mutations with Types
Generate typed mutation hooks for creating and updating posts.
Include optimistic updates.
# src/mutations/CreatePost.graphql
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
...PostCard_post
content # Additional fields for the detail view
}
}
mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
updatePost(id: $id, input: $input) {
...PostCard_post
content
updatedAt
}
}
// Generated hook usage
import { useCreatePostMutation } from '../generated/graphql-hooks';
export function CreatePostForm() {
const [createPost, { loading }] = useCreatePostMutation({
// Update Apollo cache to add new post to list
update(cache, { data }) {
if (!data?.createPost) return;
cache.modify({
fields: {
posts(existing = { items: [] }) {
const newPostRef = cache.writeFragment({
data: data.createPost,
fragment: PostCard_PostFragmentDoc, // Generated fragment document
});
return { ...existing, items: [newPostRef, ...existing.items] };
},
},
});
},
});
const handleSubmit = async (formData: CreatePostInput) => {
// createPost variables are typed — TypeScript errors if input doesn't match schema
await createPost({
variables: { input: formData },
optimisticResponse: {
createPost: {
__typename: 'Post',
id: 'temp-id',
...formData,
publishedAt: null,
author: currentUser,
},
},
});
};
Schema-First Development with Claude Code
We need to add a notification system. Start from the GraphQL schema.
Claude Code generates the schema additions first:
# New types added to schema
type Notification {
id: ID!
type: NotificationType!
message: String!
readAt: DateTime
createdAt: DateTime!
actor: User # Who triggered the notification
target: NotificationTarget # What it's about (union type)
}
union NotificationTarget = Post | Comment | User
enum NotificationType {
POST_LIKED
POST_COMMENTED
USER_FOLLOWED
MENTION
}
extend type Query {
notifications(unreadOnly: Boolean = false): [Notification!]!
unreadCount: Int!
}
extend type Mutation {
markNotificationRead(id: ID!): Notification!
markAllRead: Boolean!
}
extend type Subscription {
notificationReceived: Notification!
}
After adding this to the schema file, npm run codegen generates all the types and hooks. Then Claude Code implements the resolvers that match — starting from the schema contract rather than guessing what the client needs.
For the GraphQL subscription patterns including real-time WebSocket transport, see the GraphQL subscriptions guide. For the foundational Apollo Server setup and resolver patterns, see the GraphQL guide. The Claude Skills 360 bundle includes schema-first development skill sets with codegen configurations. Start with the free tier to set up type-safe GraphQL for your frontend.