Code Generation
Type-safe GraphQL development with graphql-codegen for TypeScript
You are an expert in GraphQL code generation, helping developers set up and use graphql-codegen to produce type-safe TypeScript types, hooks, and SDK functions from their GraphQL schemas and operations. ## Key Points 1. Codegen reads the GraphQL schema (from a file, URL, or code-first source). 2. It scans your source files for GraphQL operations (queries, mutations, fragments). 3. Plugins generate TypeScript types, hooks, or other artifacts. 4. Generated files are written to disk and imported in your application code. - name: Generate GraphQL types - name: Check for uncommitted changes 1. **Run codegen in watch mode during development** — `graphql-codegen --watch` regenerates types on schema or operation changes, giving you immediate feedback. 2. **Commit generated files** — check generated types into version control so CI can verify they are up to date and reviewers can see type changes in PRs. 3. **Use mappers for resolver types** — map GraphQL types to your database models so resolver return types reflect what your data layer actually returns. 4. **Use the `client` preset for new projects** — it combines the best features and enforces fragment masking for better component encapsulation. 5. **Define scalar mappings** — always map custom scalars to appropriate TypeScript types to avoid them defaulting to `any`. 6. **Add codegen as a pre-commit or CI step** — catch schema-operation mismatches before they reach production. ## Quick Example ```bash npm install -D @graphql-codegen/cli @graphql-codegen/typescript \ @graphql-codegen/typescript-operations \ @graphql-codegen/typescript-react-apollo \ @graphql-codegen/introspection ``` ``` src/features/posts/queries.ts # Your operations src/features/posts/queries.generated.ts # Generated types and hooks ```
skilldb get graphql-skills/Code GenerationFull skill: 406 linesCode Generation — GraphQL
You are an expert in GraphQL code generation, helping developers set up and use graphql-codegen to produce type-safe TypeScript types, hooks, and SDK functions from their GraphQL schemas and operations.
Overview
GraphQL Code Generator (graphql-codegen) reads your GraphQL schema and operations (queries, mutations, subscriptions) to produce TypeScript types, typed React hooks, resolvers signatures, and more. It eliminates manual type maintenance and catches schema-query mismatches at build time rather than runtime.
Core Concepts
How It Works
- Codegen reads the GraphQL schema (from a file, URL, or code-first source).
- It scans your source files for GraphQL operations (queries, mutations, fragments).
- Plugins generate TypeScript types, hooks, or other artifacts.
- Generated files are written to disk and imported in your application code.
Installation
npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
@graphql-codegen/typescript-operations \
@graphql-codegen/typescript-react-apollo \
@graphql-codegen/introspection
Configuration
// codegen.ts
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: ["src/**/*.{ts,tsx}"],
ignoreNoDocuments: true,
generates: {
// Shared types from the schema
"src/__generated__/types.ts": {
plugins: ["typescript"],
config: {
scalars: {
DateTime: "string",
URL: "string",
JSON: "Record<string, unknown>",
},
enumsAsTypes: true,
skipTypename: false,
avoidOptionals: {
field: true,
inputValue: false,
object: true,
},
},
},
// Operation types and hooks
"src/__generated__/operations.ts": {
plugins: ["typescript-operations", "typescript-react-apollo"],
preset: "import-types",
presetConfig: {
typesPath: "./types",
},
config: {
withHooks: true,
withHOC: false,
withComponent: false,
},
},
// Introspection result for Apollo Client cache
"src/__generated__/introspection.json": {
plugins: ["introspection"],
},
},
};
export default config;
Running Codegen
{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
}
Implementation Patterns
Typed React Hooks
With typescript-react-apollo, every operation gets a typed hook:
// src/features/posts/queries.ts
import { gql } from "@apollo/client";
export const GET_POST = gql`
query GetPost($id: ID!) {
post(id: $id) {
id
title
body
status
author {
id
displayName
avatar
}
createdAt
updatedAt
}
}
`;
export const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
post {
id
title
status
}
errors {
field
message
code
}
}
}
`;
Generated hooks are fully typed:
// In your component — full type safety, no manual typing
import { useGetPostQuery, useCreatePostMutation } from "../__generated__/operations";
function PostPage({ postId }: { postId: string }) {
const { data, loading, error } = useGetPostQuery({
variables: { id: postId }, // TypeScript enforces: { id: string }
});
// data.post is fully typed: { id: string, title: string, body: string, ... }
if (data?.post) {
return <h1>{data.post.title}</h1>;
}
}
function CreatePostForm() {
const [createPost] = useCreatePostMutation();
const handleSubmit = async (values: FormValues) => {
const { data } = await createPost({
variables: {
input: {
title: values.title, // TypeScript validates against CreatePostInput
body: values.body,
status: "DRAFT",
},
},
});
if (data?.createPost.errors.length) {
// errors is typed: Array<{ field: string[] | null, message: string, code: string }>
}
};
}
Typed Resolvers on the Server
Generate resolver type signatures to ensure your resolvers match the schema:
// codegen.ts — server-side config
const config: CodegenConfig = {
schema: "src/schema/**/*.graphql",
generates: {
"src/__generated__/resolvers-types.ts": {
plugins: ["typescript", "typescript-resolvers"],
config: {
contextType: "../context#Context",
mappers: {
User: "../models/user#UserModel",
Post: "../models/post#PostModel",
},
useIndexSignature: true,
},
},
},
};
Use the generated types in resolvers:
import type { Resolvers } from "../__generated__/resolvers-types";
// TypeScript verifies every resolver matches the schema
export const postResolvers: Resolvers = {
Query: {
post: async (_, { id }, { dataSources }) => {
return dataSources.posts.getById(id); // Return type must match PostModel
},
},
Post: {
author: (post, _, { loaders }) => {
return loaders.userById.load(post.authorId); // Must return UserModel
},
},
Mutation: {
createPost: async (_, { input }, { dataSources, currentUser }) => {
// input is typed as CreatePostInput
// return type must match CreatePostPayload
const post = await dataSources.posts.create({
...input,
authorId: currentUser.id,
});
return { post, errors: [] };
},
},
};
Fragment Colocations with Typed Components
// UserAvatar.tsx
import { gql } from "@apollo/client";
import type { UserAvatarFragment } from "../__generated__/operations";
export const USER_AVATAR_FRAGMENT = gql`
fragment UserAvatar on User {
id
displayName
avatar
}
`;
// Props are typed from the fragment
export function UserAvatar({ user }: { user: UserAvatarFragment }) {
return <img src={user.avatar ?? "/default-avatar.png"} alt={user.displayName} />;
}
Near-Operation-File Preset
Generate types next to each operation file instead of one monolithic file:
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: "src/**/*.{ts,tsx}",
generates: {
"src/": {
preset: "near-operation-file",
presetConfig: {
extension: ".generated.ts",
baseTypesPath: "__generated__/types.ts",
},
plugins: ["typescript-operations", "typescript-react-apollo"],
},
},
};
This produces files like:
src/features/posts/queries.ts # Your operations
src/features/posts/queries.generated.ts # Generated types and hooks
Custom Scalars
Map GraphQL scalars to TypeScript types:
config: {
scalars: {
DateTime: "string", // ISO 8601 string
Date: "string", // YYYY-MM-DD
JSON: "Record<string, unknown>",
URL: "string",
EmailAddress: "string",
BigInt: "bigint",
Upload: "File",
Void: "void",
},
}
CI Integration
Add codegen validation to CI to catch schema drift:
# .github/workflows/ci.yml
- name: Generate GraphQL types
run: npm run codegen
- name: Check for uncommitted changes
run: |
git diff --exit-code src/__generated__/
if [ $? -ne 0 ]; then
echo "Generated types are out of date. Run 'npm run codegen' and commit."
exit 1
fi
Client Preset (Newer Approach)
The client preset is the recommended approach for new projects, combining multiple plugins:
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql",
documents: ["src/**/*.tsx"],
generates: {
"./src/__generated__/": {
preset: "client",
config: {
fragmentMasking: { unmaskFunctionName: "getFragmentData" },
},
},
},
};
Usage with the client preset:
import { graphql } from "../__generated__/gql";
import { getFragmentData } from "../__generated__";
const UserFragment = graphql(`
fragment UserFields on User {
id
displayName
email
}
`);
const GetUsersQuery = graphql(`
query GetUsers {
users {
...UserFields
}
}
`);
function UserList() {
const { data } = useQuery(GetUsersQuery);
const users = data?.users.map((u) => getFragmentData(UserFragment, u));
// users is typed correctly
}
Core Philosophy
GraphQL Code Generation exists to enforce a single source of truth: the schema defines the API contract, and every type in your application — client-side query results, server-side resolver signatures, fragment props — is mechanically derived from that contract. When a field is renamed in the schema, codegen produces a compile-time error in every file that references it. This tight feedback loop collapses the distance between schema change and bug detection from "runtime in production" to "IDE red squiggle before you save."
The deepest benefit of codegen is not convenience but correctness. Handwritten types for GraphQL responses inevitably drift from the schema — a nullable field gets typed as required, a new enum value gets missed, a deprecated field lingers in the type definition. Codegen eliminates this class of bug entirely. It also makes the codebase self-documenting: the generated types are always accurate, so developers can trust autocompletion and hover-documentation without cross-referencing the schema manually.
Adoption succeeds when codegen is treated as infrastructure, not tooling. It should run in watch mode during development so types update instantly, commit its output to version control so diffs are visible in code review, and validate freshness in CI so stale types never reach production. When codegen is woven into the development workflow rather than bolted on as an afterthought, it becomes invisible — developers just experience a codebase where GraphQL types are always correct.
Anti-Patterns
-
Maintaining handwritten types alongside generated ones — duplicating types that codegen already produces creates two sources of truth that inevitably diverge. Import from the generated file or do not use codegen at all; half-measures create confusion.
-
Running codegen only manually before releases — types that are stale during development provide false confidence. Schema changes silently break at runtime because TypeScript saw the old types. Run codegen in watch mode or as a pre-commit hook.
-
One monolithic generated file for a large project — a single 10,000+ line generated file slows the TypeScript language server and makes code review painful. Use the
near-operation-fileorclientpreset to distribute generated types next to the operations that use them. -
Leaving custom scalars unmapped — unmapped scalars default to
any, which silently disables type checking for every field using that scalar. Always mapDateTime,JSON,URL, and other custom scalars to concrete TypeScript types. -
Ignoring codegen output in code review — generated type diffs reveal schema changes that might otherwise go unnoticed. Reviewing these diffs catches breaking changes, unnecessary field additions, and nullability shifts before they reach production.
Best Practices
- Run codegen in watch mode during development —
graphql-codegen --watchregenerates types on schema or operation changes, giving you immediate feedback. - Commit generated files — check generated types into version control so CI can verify they are up to date and reviewers can see type changes in PRs.
- Use mappers for resolver types — map GraphQL types to your database models so resolver return types reflect what your data layer actually returns.
- Use the
clientpreset for new projects — it combines the best features and enforces fragment masking for better component encapsulation. - Define scalar mappings — always map custom scalars to appropriate TypeScript types to avoid them defaulting to
any. - Add codegen as a pre-commit or CI step — catch schema-operation mismatches before they reach production.
Common Pitfalls
- Forgetting to rerun codegen after schema changes — types become stale and TypeScript won't catch real errors. Use watch mode or CI checks to prevent drift.
- Using
anyfor unmapped scalars — custom scalars default toanyif not mapped, defeating the purpose of type generation. - Monolithic generated files on large projects — a single 10,000-line generated file slows down the TypeScript language server. Use
near-operation-fileorclientpreset to split output. - Not using fragment colocation — without fragments, components depend on their parent's query shape. Fragment colocation makes each component's data requirements explicit and typed.
- Mixing handwritten and generated types — maintain a single source of truth. If types come from codegen, do not duplicate them manually; import from the generated file.
Install this skill directly: skilldb add graphql-skills
Related Skills
Apollo Client
Apollo Client with React for querying, mutating, and caching GraphQL data
Apollo Server
Apollo Server setup, configuration, plugins, and production deployment patterns
Authentication
Authentication and authorization patterns for securing GraphQL APIs
Pagination
Cursor-based pagination following the Relay connection specification
Resolvers
Resolver patterns, data loading strategies, and the N+1 problem in GraphQL
Schema Design
Schema design principles for building maintainable, intuitive GraphQL APIs