Skip to main content
Technology & EngineeringGraphql406 lines

Code Generation

Type-safe GraphQL development with graphql-codegen for TypeScript

Quick Summary32 lines
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 lines
Paste into your CLAUDE.md or agent config

Code 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

  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.

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-file or client preset 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 map DateTime, 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

  1. Run codegen in watch mode during developmentgraphql-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.

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 any for unmapped scalars — custom scalars default to any if 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-file or client preset 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

Get CLI access →