MinhVo

Minh Vo

rss feed

Slaying code & making it lit fr fr 🔥 tagline

Hey there 👋 I'm an AI Engineer with 7 years of experience building scalable web and mobile applications. Currently at Neurond AI (May 2025 — present), architecting an Enterprise AI Assistant Platform with multi-tenant RAG on pgvector, multi-provider LLM orchestration, and Azure-native infrastructure. Previously spent 5+ years at SNAPTEC (Sep 2019 — Apr 2025), leading SaaS themes, admin dashboards, and e-commerce platforms — earned the Hero of the Year award in 2021. I specialize in TypeScript, React, Next.js, and AI-Native engineering with Claude Code and Cursor.bio

Back to blogs

GraphQL Code Generator: Type-Safe GraphQL in TypeScript

Generate TypeScript types from GraphQL schemas: queries, mutations, and fragments.

GraphQLCode GeneratorTypeScriptAPI

By MinhVo

Introduction

GraphQL's type system is one of its greatest strengths, but without proper tooling, TypeScript developers often end up manually writing interface definitions that drift out of sync with the actual schema. GraphQL Code Generator solves this by automatically generating TypeScript types from your GraphQL schema and operations, creating a compile-time guarantee that your client code matches the server's API exactly.

In this comprehensive guide, we'll set up GraphQL Code Generator from scratch, explore its plugin architecture, implement patterns that scale from small projects to enterprise applications with hundreds of operations, and cover advanced techniques like fragment masking, custom scalars, and validation schema generation.

TypeScript and GraphQL integration

The Problem: Manual Type Definitions Are Fragile

Without code generation, TypeScript developers face a painful choice. They can write manual interfaces for every GraphQL response, which requires constant maintenance as the schema evolves. Or they can use any types and lose the benefits of TypeScript entirely. Both approaches lead to bugs—either stale types that don't match reality or no types at all.

Consider a typical GraphQL query without code generation:

// ❌ Manual type definition - must be updated every time the schema changes
interface GetUserQuery {
  user: {
    id: string;
    name: string;
    email: string;
    avatar?: string;
    role: string;
    posts: Array<{
      id: string;
      title: string;
      publishedAt: string;
    }>;
  };
}

When the server adds a new field or renames an existing one, every manually written interface becomes stale. TypeScript won't catch the mismatch because the manual type is just an assertion—it's not derived from the actual schema. The result is runtime errors that could have been caught at compile time.

How GraphQL Code Generator Works

GraphQL Code Generator eliminates this tradeoff. It reads your schema, reads your operations, and produces TypeScript types that exactly match what the server will return. When a field is added to the schema, running codegen updates the types. When you rename a field in a query, the generated types change accordingly. TypeScript's compiler then catches any mismatches at build time.

Input Sources

Codegen requires two inputs: the schema and the operations. The schema can come from multiple sources:

  • Introspection endpoint: http://localhost:4000/graphql
  • Local SDL files: ./schema.graphql or ./schema/**/*.graphql
  • JSON introspection result: ./schema.json
  • Multiple sources: Array of URLs or files for federated schemas

Operations are found by scanning your source code for .graphql files or gql tagged template literals. The documents configuration option uses glob patterns to specify where to find them.

Output Artifacts

The generator produces several types of output depending on the plugins used:

// Base types (from typescript plugin)
export type User = {
  __typename?: "User";
  id: Scalars["ID"];
  name: Scalars["String"];
  email: Scalars["String"];
};
 
// Operation types (from typescript-operations plugin)
export type GetUserQuery = {
  __typename?: "Query";
  user: Pick<User, "id" | "name" | "email">;
};
 
// React hooks (from typescript-react-apollo plugin)
export function useGetUserQuery(
  options?: Apollo.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
  return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(
    GetUserDocument,
    options
  );
}

Generated code in IDE

Architecture and Design Patterns

Plugin Pipeline Architecture

Codegen processes your schema and operations through a plugin pipeline. Each plugin receives the output of the previous plugin and adds its own transformations. The typescript plugin generates base types, typescript-operations extends those with operation-specific types, and framework plugins add hooks or clients.

// Pipeline: schema → typescript → typescript-operations → typescript-react-apollo
// Each plugin adds more to the output

This architecture allows you to mix and match plugins to generate exactly what your project needs. A Node.js backend might use typescript and typescript-graphql-request, while a React frontend uses typescript-react-apollo.

Preset System

Presets are pre-configured plugin combinations that generate output into multiple files. The client preset is the most common, generating a single output file with all types and a typed gql function:

import { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  schema: "./schema.graphql",
  documents: ["src/**/*.tsx"],
  generates: {
    "./src/gql/": {
      preset: "client",
    },
  },
};

The near-operation-file preset generates types next to each operation file, enabling better code organization in large projects.

Fragment Masking

Fragment masking is a pattern where components receive typed fragments rather than full query results. This creates clean component APIs where each component only sees the data it declared in its fragment:

// Component receives only its fragment's data
function UserAvatar({ user }: { user: FragmentType<typeof UserAvatarFragment> }) {
  const data = useFragment(UserAvatarFragment, user);
  return <img src={data.avatar} alt={data.name} />;
}

Fragment masking encourages colocation—each component defines the data it needs, and the parent query aggregates fragments from all child components. This pattern scales elegantly as your component tree grows.

Step-by-Step Implementation

Initial Setup

Install the required packages and initialize the configuration:

# Install core packages
npm install -D @graphql-codegen/cli \
  @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-react-apollo
 
# Or use the client preset (recommended for new projects)
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
 
# Generate initial config
npx graphql-codegen init

Writing the Configuration

Create codegen.ts with your project-specific settings:

import { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  // Schema source
  schema: "http://localhost:4000/graphql",
 
  // Where to find operations
  documents: ["src/**/*.{ts,tsx}", "!src/generated/**"],
 
  // Output configuration
  generates: {
    "./src/generated/": {
      preset: "client",
      presetConfig: {
        gqlTagName: "gql",
        fragmentMasking: true,
      },
    },
  },
 
  // Format generated files
  hooks: {
    afterAllFileWrite: ["prettier --write"],
  },
};
 
export default config;

Defining Operations

Write your GraphQL operations in .graphql files or as tagged template literals. Each operation generates its own TypeScript type:

# src/operations/user.graphql
fragment UserFields on User {
  id
  name
  email
  avatar
  role
}
 
query GetUser($id: ID!) {
  user(id: $id) {
    ...UserFields
    posts {
      id
      title
      publishedAt
    }
  }
}
 
mutation UpdateUser($id: ID!, $input: UpdateUserInput!) {
  updateUser(id: $id, input: $input) {
    ...UserFields
  }
}

Running Generation

Execute codegen to generate types. In development, use watch mode for automatic regeneration:

# One-time generation
npx graphql-codegen
 
# Watch mode
npx graphql-codegen --watch
 
# Debug mode (shows detailed output)
npx graphql-codegen --config codegen.ts --verbose

Using Generated Types

Import and use the generated types in your components. TypeScript will enforce type safety across your entire API layer:

import { useGetUserQuery, useUpdateUserMutation } from "../generated/graphql";
import { graphql } from "../generated";
 
const GetUserDocument = graphql(`
  query GetUser($id: ID!) {
    user(id: $id) {
      ...UserFields
    }
  }
`);
 
function UserProfile({ userId }: { userId: string }) {
  const { data, loading, error } = useGetUserQuery({
    variables: { id: userId },
  });
 
  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;
 
  const { user } = data;
 
  return (
    <div>
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <span>{user.role}</span>
      <h2>Posts</h2>
      {user.posts.map((post) => (
        <article key={post.id}>
          <h3>{post.title}</h3>
          <time>{post.publishedAt}</time>
        </article>
      ))}
    </div>
  );
}

Framework-Specific Plugins

GraphQL Code Generator provides plugins for every major frontend framework. Each generates framework-specific hooks or components that integrate seamlessly with your application.

React with Apollo Client

// Generated hooks for React
export function useGetUsersQuery(
  options?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>
) {
  return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(
    GetUsersDocument,
    options
  );
}
 
export function useCreateUserMutation(
  options?: Apollo.MutationHookOptions<CreateUserMutation, CreateUserMutationVariables>
) {
  return Apollo.useMutation<CreateUserMutation, CreateUserMutationVariables>(
    CreateUserDocument,
    options
  );
}

React with TanStack Query

For teams using TanStack Query instead of Apollo, the typescript-react-query plugin generates typed query functions:

import { useQuery } from "@tanstack/react-query";
import { useGetUsersQuery } from "./generated";
 
function UserList() {
  const { data, isLoading, error } = useGetUsersQuery();
  // data is fully typed as GetUsersQuery
}

Vue with Apollo

The typescript-vue-apollo plugin generates Vue composition functions:

import { useGetUsersQuery } from "./generated";
 
export default defineComponent({
  setup() {
    const { result, loading, error } = useGetUsersQuery();
    return { result, loading, error };
  },
});

Angular with Apollo

Angular developers get typed observable queries:

import { GetUsersGQL } from "./generated";
 
@Component({ /* ... */ })
class UserListComponent implements OnInit {
  users: User[];
 
  constructor(private getUsersGQL: GetUsersGQL) {}
 
  ngOnInit() {
    this.getUsersGQL.watch().valueChanges.subscribe(({ data }) => {
      this.users = data.users;
    });
  }
}

Svelte with Apollo

The typescript-svelte-apollo plugin generates Svelte-compatible reactive queries:

<script lang="ts">
  import { GetUsers } from "./generated";
 
  $: result = GetUsers();
</script>
 
{#each $result?.data?.users || [] as user}
  <div>{user.name}</div>
{/each}

Real-World Use Cases

Large-Scale SaaS Application

A SaaS platform with 200+ operations uses the near-operation-file preset to generate types alongside each feature module. Each team owns their feature's GraphQL operations, and codegen ensures cross-team type consistency. When the shared User type changes, every team's codegen run surfaces the exact components that need updating.

Mobile App with Shared Schema

A React Native app and web dashboard share the same GraphQL schema. Codegen runs once during the build process and generates types that both platforms consume. The shared type definitions eliminate the possibility of the mobile app sending different field selections than the web app expects.

Microservices with Federation

In a federated architecture, each microservice generates types from its own subgraph. A gateway service generates combined types from the supergraph. Codegen's federation support handles @key, @external, and @requires directives correctly, generating types that reflect the merged schema.

Validation Schema Generation

One powerful but lesser-known feature is generating validation schemas from your GraphQL input types. The typescript-validation-schema plugin generates Zod, Yup, or Valibot schemas that match your GraphQL inputs exactly:

// codegen.ts
import { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  schema: "./schema.graphql",
  generates: {
    "./src/generated/graphql.ts": {
      plugins: ["typescript"],
    },
    "./src/generated/validation.ts": {
      plugins: [
        {
          "typescript-validation-schema": {
            schema: "zod",
            importFrom: "./graphql",
            scalarSchemas: {
              DateTime: "z.string().datetime()",
              Email: "z.string().email()",
            },
          },
        },
      ],
    },
  },
};

This generates Zod schemas that validate form inputs before they reach your GraphQL mutations:

import { CreatePostInputSchema } from "./generated/validation";
 
// Validate form data against the GraphQL input type
const result = CreatePostInputSchema.safeParse(formData);
if (!result.success) {
  console.error(result.error.issues);
}

Best Practices for Production

  1. Use the client preset for new projects: It provides the simplest API with automatic fragment masking and optimized bundle sizes.

  2. Commit generated files: Include generated files in version control so all developers have consistent types without running codegen.

  3. Add --check to CI: Run npx graphql-codegen --check in CI to verify generated files match the current schema and operations.

  4. Use fragment colocation: Place fragments in the same file as the component that uses them, or in a dedicated fragments.graphql file per feature.

  5. Configure custom scalars: Map GraphQL scalars to appropriate TypeScript types to avoid losing type information:

config: {
  scalars: {
    DateTime: "string",
    JSON: "Record<string, unknown>",
    Upload: "File",
  },
}
  1. Exclude generated files from linting: Add src/generated/ to your .eslintignore to avoid linting auto-generated code.

  2. Use enumsAsTypes for better tree-shaking: Generate string literal union types instead of TypeScript enums.

  3. Set up IDE integration: Install the GraphQL extension for VS Code to get IntelliSense on .graphql files and inline operations.

Common Pitfalls and Solutions

PitfallImpactSolution
Schema not reachable at build timeCodegen fails in CIUse local schema files instead of introspection
Generated types too largeSlow TypeScript compilationSplit generation by domain
Circular fragment referencesCodegen hangs or errorsRestructure fragments to avoid cycles
Missing __typename in operationsIncomplete generated typesAdd addTypename: true to client config
Enum values not matching serverRuntime errorsUse enumValues config for custom mapping
Custom scalars defaulting to anyLoss of type safetyAlways configure scalars mapping

Performance Optimization

For large projects, codegen can become slow. Optimize by limiting the scope of documents globs, using the near-operation-file preset to split output, and running codegen in parallel for different schema sections:

// Split generation for faster builds
const config: CodegenConfig = {
  generates: {
    "./src/features/users/generated/": {
      schema: "./schema.graphql",
      documents: "src/features/users/**/*.graphql",
      plugins: ["typescript", "typescript-operations"],
    },
    "./src/features/products/generated/": {
      schema: "./schema.graphql",
      documents: "src/features/products/**/*.graphql",
      plugins: ["typescript", "typescript-operations"],
    },
  },
};

Comparison with Alternatives

FeatureGraphQL CodegentRPCREST + Swagger Codegen
Type generationAutomaticInferred from TSAutomatic
Schema formatGraphQL SDLTypeScriptOpenAPI YAML/JSON
Client hooksGeneratedBuilt-inManual
Multi-frameworkYesNext.js focusedLimited
Fragment supportYesN/AN/A
Plugin ecosystemRichLimitedModerate

Advanced Patterns

Typed Fragments for Component APIs

Generate typed fragment references to create strongly-typed component props:

const UserAvatarFragment = graphql(`
  fragment UserAvatar on User {
    id
    name
    avatar
  }
`);
 
function UserAvatar({ user }: { user: FragmentType<typeof UserAvatarFragment> }) {
  const data = useFragment(UserAvatarFragment, user);
  return <img src={data.avatar} alt={data.name} />;
}

Custom Codegen Plugins

For project-specific needs, write custom plugins that generate additional artifacts:

// Custom plugin: generates mock factories
const plugin: CodegenPlugin = {
  plugin: (schema, documents, config) => {
    return documents.map((doc) => {
      const operation = doc.operations[0];
      return `
        export const mock${operation.name} = (overrides?: Partial<${operation.name}Query>) => ({
          ...defaults,
          ...overrides,
        });
      `;
    });
  },
};

Testing Generated Types

Generated types enable a testing strategy where type correctness is verified at compile time rather than runtime. Write tests that construct GraphQL response objects using the generated types, ensuring that your components handle all possible response shapes correctly:

import { graphql } from "./generated";
 
describe("Generated GraphQL types", () => {
  it("generates correct query types", () => {
    const document = graphql(`
      query TestQuery {
        users {
          id
          name
        }
      }
    `);
    expect(document.definitions).toHaveLength(1);
  });
});

Use the generated fragment types to create test fixtures that match the exact shape of real API responses. This eliminates the common testing problem where test data diverges from the actual API response shape over time. Generate mock data factories from your GraphQL schema that produce type-safe test data, ensuring that your mocks always match the current schema.

TypeScript code on screen

Future Outlook

GraphQL Code Generator is moving toward zero-config setups with automatic schema detection and intelligent plugin selection. The client preset already simplifies the developer experience significantly. Future developments include better support for server components, streaming subscriptions with @defer and @stream, and automatic resolver type generation.

The ecosystem continues to grow with community plugins for validation schemas, mock generation, and framework-specific integrations. As GraphQL adoption increases, code generation becomes an essential part of the TypeScript GraphQL stack.

Conclusion

GraphQL Code Generator is an essential tool for any TypeScript project using GraphQL. It eliminates manual type definitions, catches API mismatches at compile time, and generates framework-specific hooks that provide full IntelliSense support. The key takeaways are:

  1. Use the client preset for the simplest setup with fragment masking
  2. Commit generated files and verify them in CI with --check
  3. Use fragment colocation for scalable type management
  4. Configure custom scalars to preserve type information
  5. Split generation by domain for faster builds in large projects
  6. Generate validation schemas to validate inputs before they reach your API

The investment in setting up codegen pays off immediately by eliminating an entire category of bugs and dramatically improving the developer experience when working with GraphQL APIs.