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 Generation: Type-Safe APIs

Generate types from GraphQL schemas: codegen setup, typed hooks, and CI integration.

GraphQLCode GenerationTypeScriptAPI

By MinhVo

Introduction

One of the most compelling advantages of GraphQL is its strongly-typed schema. Yet without proper tooling, TypeScript developers often find themselves manually typing API responses, creating a disconnect between the schema and the client code. GraphQL Code Generator bridges this gap by automatically generating TypeScript types, React hooks, and other artifacts directly from your GraphQL schema and operations.

This eliminates an entire class of bugs where your client code assumes a field exists or has a certain type, only to fail at runtime. With code generation, every query, mutation, and subscription produces fully-typed results that stay synchronized with your server schema.

Code generation workflow diagram

Understanding Code Generation: Core Concepts

How GraphQL Code Generator Works

GraphQL Code Generator (codegen) operates by combining three inputs: your GraphQL schema, your operations (queries, mutations, subscriptions), and a configuration file. It parses all three, validates operations against the schema, and generates TypeScript (or other language) types as output.

The schema can be introspected from a running GraphQL server, loaded from local .graphql files, or extracted from a schema registry. Operations are typically .graphql files or template literals tagged with gql in your source code. The configuration file (codegen.ts or codegen.yml) specifies which plugins to use and how to generate the output.

The Plugin Architecture

Codegen's power comes from its plugin system. Each plugin generates a specific artifact:

  • typescript: Base TypeScript types for all schema types
  • typescript-operations: Types for your specific queries and mutations
  • typescript-react-apollo: Typed React hooks (useQuery, useMutation)
  • typescript-graphql-request: Typed GraphQL client for Node.js
  • typed-document-node: Typed document nodes for framework-agnostic usage

You can combine multiple plugins to generate all the artifacts your application needs from a single configuration.

Schema-First vs. Code-First

GraphQL Code Generator works best with schema-first approaches where your schema is defined in .graphql SDL files. For code-first frameworks like TypeGraphQL or NestJS, codegen can still work by extracting the schema from the running server or generating SDL files as a build step.

Architecture and Design Patterns

The Generation Pipeline

The codegen pipeline follows a clear architecture: input collection β†’ schema parsing β†’ operation validation β†’ type generation β†’ file output. Each step is handled by plugins that transform the data in sequence.

// codegen.ts configuration
import { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  schema: "http://localhost:4000/graphql",
  documents: "src/**/*.graphql",
  generates: {
    "src/generated/graphql.ts": {
      plugins: ["typescript", "typescript-operations"],
    },
    "src/generated/hooks.tsx": {
      plugins: ["typescript-react-apollo"],
      config: {
        withHooks: true,
        withComponent: false,
        withHOC: false,
      },
    },
  },
};
 
export default config;

Fragment Colocation Pattern

The fragment colocation pattern places GraphQL fragments alongside the components that use them. This creates a clear ownership model where each component declares exactly what data it needs:

# UserProfile.graphql
fragment UserBasicFields on User {
  id
  name
  email
  avatar
}
 
query GetUser($id: ID!) {
  user(id: $id) {
    ...UserBasicFields
    role
    createdAt
  }
}

This pattern scales beautifully because components can import and compose fragments from other components, and codegen generates types for every fragment combination automatically.

The Generated Types Structure

Understanding the structure of generated types helps you work effectively with codegen output. For a typical schema, codegen generates:

// Base schema types
export type User = {
  __typename?: "User";
  id: Scalars["ID"];
  name: Scalars["String"];
  email: Scalars["String"];
  posts: Array<Post>;
};
 
// Operation-specific types
export type GetUserQuery = {
  __typename?: "Query";
  user: {
    __typename?: "User";
    id: string;
    name: string;
    posts: Array<{
      __typename?: "Post";
      id: string;
      title: string;
    }>;
  };
};
 
// Hook types
export function useGetUserQuery(
  options: Apollo.QueryHookOptions<GetUserQuery, GetUserQueryVariables>
) {
  return Apollo.useQuery<GetUserQuery, GetUserQueryVariables>(
    GetUserDocument,
    options
  );
}

TypeScript code generation output

Step-by-Step Implementation

Installing Codegen Dependencies

Start by installing the required packages. The core CLI and plugins are all under the @graphql-codegen scope:

npm install -D @graphql-codegen/cli @graphql-codegen/typescript \
  @graphql-codegen/typescript-operations \
  @graphql-codegen/typescript-react-apollo
 
# Initialize configuration
npx graphql-codegen init

Configuring the Generator

Create a codegen.ts file at your project root. The configuration specifies where to find the schema, which files contain operations, and what artifacts to generate:

import { CodegenConfig } from "@graphql-codegen/cli";
 
const config: CodegenConfig = {
  schema: "./schema.graphql",
  documents: ["src/**/*.{ts,tsx}", "!src/generated/**"],
  generates: {
    "./src/generated/": {
      preset: "client",
      plugins: [],
      presetConfig: {
        gqlTagName: "gql",
      },
    },
  },
  hooks: {
    afterAllFileWrite: ["prettier --write"],
  },
};
 
export default config;

Writing Typed Operations

With codegen configured, write your GraphQL operations using the gql tag. The generated types will be inferred from these operations:

import { gql } from "./generated";
 
export const GET_USERS = gql(`
  query GetUsers($limit: Int, $offset: Int) {
    users(limit: $limit, offset: $offset) {
      id
      name
      email
      role
    }
  }
`);
 
export const CREATE_USER = gql(`
  mutation CreateUser($input: CreateUserInput!) {
    createUser(input: $input) {
      id
      name
      email
    }
  }
`);

Using Generated Hooks

After running codegen, use the generated hooks in your React components. Every parameter and return value is fully typed:

import { useGetUsersQuery, useCreateUserMutation } from "./generated";
 
function UserList() {
  const { data, loading, error } = useGetUsersQuery({
    variables: { limit: 10, offset: 0 },
  });
 
  const [createUser] = useCreateUserMutation({
    refetchQueries: [{ query: GetUsersDocument }],
  });
 
  if (loading) return <Spinner />;
  if (error) return <ErrorDisplay error={error} />;
 
  return (
    <div>
      {data.users.map((user) => (
        <div key={user.id}>
          <h3>{user.name}</h3>
          <p>{user.email}</p>
          <span>{user.role}</span>
        </div>
      ))}
    </div>
  );
}

Running Code Generation

Execute codegen with the CLI. During development, watch mode regenerates types whenever your schema or operations change:

# One-time generation
npx graphql-codegen
 
# Watch mode for development
npx graphql-codegen --watch
 
# With specific config file
npx graphql-codegen --config codegen.ts

Codegen CI pipeline diagram

Real-World Use Cases

E-Commerce Platform

An e-commerce platform uses codegen to generate types for product catalogs, shopping carts, and checkout flows. Each domain has its own set of .graphql files colocated with the components that use them. When the product team adds a new field to the Product type, all client code automatically gets the new type, and TypeScript errors show exactly which components need updating.

SaaS Dashboard

A SaaS application with complex reporting features generates typed hooks for each report type. The generated useDashboardMetricsQuery hook returns fully typed metric objects, making it trivial to build chart components that consume the data without any manual type assertions.

Multi-Platform Mobile App

A React Native app shares its GraphQL operations with a web frontend. Codegen generates both React Apollo hooks for the web and generic typed document nodes that work with any GraphQL client, enabling code sharing across platforms while maintaining full type safety.

Best Practices for Production

  1. Commit generated files to version control: This ensures all developers have the latest types and eliminates a build step during development.

  2. Run codegen in CI with --check mode: Add npx graphql-codegen --check to your CI pipeline to verify that generated files are up to date.

  3. Use fragment colocation: Place fragments next to components for better code organization and automatic tree-shaking of unused types.

  4. Configure gqlTagName for optimal tree-shaking: Use gql from the generated module instead of @apollo/client to enable better dead code elimination.

  5. Set up afterAllFileWrite hooks: Run Prettier or ESLint on generated files to maintain consistent formatting.

  6. Use preset: "client" for modern setups: The client preset simplifies configuration and generates a more compact, modern API.

  7. Separate schema and operations generation: Generate base types and operation types in separate outputs for cleaner imports.

  8. Document your codegen configuration: Add comments explaining plugin choices and configuration options for team members.

Common Pitfalls and Solutions

PitfallImpactSolution
Generated files out of syncRuntime type mismatchesAdd --check to CI, use watch mode locally
Fragment spread depth errorsCodegen fails with cryptic errorsCheck for circular fragment references
Enum naming conflictsTypeScript compilation errorsConfigure enumValues in codegen config
Large generated filesSlow IDE performanceSplit generation by domain/feature
Missing __typenameNormalization breaksAlways include __typename in operations

Performance Optimization

Codegen performance matters in large projects with hundreds of operations. Use the documents glob to exclude test files and node_modules, configure incremental generation to only regenerate changed files, and run codegen as a background process during development.

// Optimize codegen config for large projects
const config: CodegenConfig = {
  schema: "./schema.graphql",
  documents: [
    "src/features/**/operations.graphql",
    "!src/**/*.test.ts",
    "!src/**/*.spec.ts",
  ],
  generates: {
    "./src/generated/types.ts": {
      plugins: ["typescript", "typescript-operations"],
      config: {
        skipTypename: true, // Reduces generated type size
        enumsAsTypes: true, // Better tree-shaking
      },
    },
  },
};

Comparison with Alternatives

FeatureGraphQL CodegentRPCREST + OpenAPI
Schema sourceGraphQL SDLTypeScript typesOpenAPI spec
Type generationAutomaticFrom TypeScriptCodegen needed
Client hooksGeneratedBuilt-inManual
Multi-languageYesTypeScript onlyMultiple
Learning curveModerateLowLow
Ecosystem maturityVery matureGrowingVery mature

Advanced Patterns

Custom Scalars

Define custom scalar types to map GraphQL scalars to TypeScript types:

const config: CodegenConfig = {
  generates: {
    "src/generated/graphql.ts": {
      plugins: ["typescript"],
      config: {
        scalars: {
          DateTime: "Date",
          JSON: "Record<string, unknown>",
          Email: "string",
        },
      },
    },
  },
};

Schema Stitching Support

For federated or stitched schemas, codegen can generate types that span multiple services. Use the @graphql-codegen/schema-ast plugin to extract schemas from each service and combine them:

const config: CodegenConfig = {
  schema: [
    "http://localhost:4001/graphql",
    "http://localhost:4002/graphql",
  ],
  generates: {
    "./src/generated/combined.ts": {
      plugins: ["typescript", "typescript-operations"],
    },
  },
};

Typed Document Nodes for Framework-Agnostic Usage

The typed-document-node plugin generates framework-agnostic typed document nodes that work with any GraphQL client. This is particularly useful when sharing operations between React, Vue, Svelte, or vanilla JavaScript:

import { TypedDocumentNode } from "@graphql-codegen/typed-document-node";
 
// Generated typed document node
export const GetUserDocument: TypedDocumentNode<
  GetUserQuery,
  GetUserQueryVariables
> = {
  kind: Kind.DOCUMENT,
  definitions: [
    /* ... */
  ],
};
 
// Works with any GraphQL client
const result = await client.request(GetUserDocument, { id: "1" });
// result is fully typed as GetUserQuery

Persisted Queries Generation

Generate persisted query manifests to reduce network overhead and improve security. The manifest maps query hashes to query strings, allowing the server to reject arbitrary queries:

const config: CodegenConfig = {
  generates: {
    "./src/generated/persisted-documents.json": {
      plugins: ["graphql-codegen-persisted-query-ids"],
      config: {
        output: "apollo",
        algorithm: "sha256",
      },
    },
  },
};

With persisted queries enabled, the client sends only the query hash instead of the full query string. This reduces request payload size by 60-80% for complex queries and prevents attackers from executing arbitrary GraphQL operations against your API.

CI/CD Integration with GitHub Actions

Automate codegen validation in your CI pipeline to catch schema drift before it reaches production:

# .github/workflows/graphql-codegen.yml
name: GraphQL Codegen Check
on: [push, pull_request]
 
jobs:
  codegen:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - name: Generate types
        run: npx graphql-codegen
      - name: Check for uncommitted changes
        run: |
          if [ -n "$(git status --porcelain src/generated/)" ]; then
            echo "Generated types are out of date. Run 'npx graphql-codegen' and commit the changes."
            git diff src/generated/
            exit 1
          fi

This workflow runs on every push and pull request, ensuring that generated types are always in sync with the schema. If a developer forgets to regenerate types after changing the schema, the CI check fails and provides clear instructions on how to fix it.

Testing Strategies

Test your codegen configuration by verifying that generated types match your expectations. Create snapshot tests that compare generated output to expected baselines:

import { readFileSync } from "fs";
 
describe("GraphQL Codegen", () => {
  it("generates correct types for User query", () => {
    const generated = readFileSync("src/generated/graphql.ts", "utf-8");
    expect(generated).toContain("export type GetUserQuery");
    expect(generated).toContain("user: {");
  });
});

Vue.js and Angular Integration

GraphQL Code Generator supports multiple frontend frameworks through dedicated plugins. For Vue.js applications using Apollo Vue, generate typed composables:

// codegen.ts for Vue
const config: CodegenConfig = {
  generates: {
    "./src/generated/graphql.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-vue-apollo",
      ],
      config: {
        withCompositionFunctions: true,
        vueApolloComposableImportFrom: "@vue/apollo-composable",
      },
    },
  },
};
 
// Usage in Vue component
import { useGetUsersQuery } from "@/generated/graphql";
 
export default defineComponent({
  setup() {
    const { result, loading, error } = useGetUsersQuery({
      limit: 10,
    });
 
    return { users: computed(() => result.value?.users ?? []), loading, error };
  },
});

For Angular applications using Apollo Angular, generate typed services:

// codegen.ts for Angular
const config: CodegenConfig = {
  generates: {
    "./src/generated/graphql.ts": {
      plugins: [
        "typescript",
        "typescript-operations",
        "typescript-apollo-angular",
      ],
    },
  },
};
 
// Usage in Angular component
@Injectable({ providedIn: "root" })
export class UserService {
  constructor(private getUsersGQL: GetUsersGQL) {}
 
  getUsers(limit: number) {
    return this.getUsersGQL.watch({ limit }).valueChanges.pipe(
      map((result) => result.data.users)
    );
  }
}

Handling Unions and Interfaces

GraphQL unions and interfaces generate discriminated union types in TypeScript, enabling exhaustive pattern matching:

union SearchResult = User | Post | Comment
 
interface Node {
  id: ID!
}
 
type User implements Node {
  id: ID!
  name: String!
  email: String!
}
 
type Post implements Node {
  id: ID!
  title: String!
  content: String!
}
// Generated discriminated union
export type SearchResult =
  | { __typename: "User"; id: string; name: string; email: string }
  | { __typename: "Post"; id: string; title: string; content: string }
  | { __typename: "Comment"; id: string; text: string };
 
// Exhaustive switch with TypeScript
function renderResult(result: SearchResult) {
  switch (result.__typename) {
    case "User":
      return <UserCard name={result.name} email={result.email} />;
    case "Post":
      return <PostCard title={result.title} content={result.content} />;
    case "Comment":
      return <CommentCard text={result.text} />;
    default:
      // TypeScript ensures all cases are handled
      const _exhaustive: never = result;
      return null;
  }
}

This pattern catches missing cases at compile time. If a new type is added to the union, TypeScript flags every switch statement that doesn't handle it.

Future Outlook

The GraphQL code generation ecosystem is evolving toward zero-configuration setups where types are generated automatically based on your project structure. The client preset already simplifies this significantly. Future developments include better support for server components, streaming operations, and automatic fragment composition.

The rise of GraphQL federation and schema composition tools like Apollo Federation and Hive is driving codegen toward unified type generation across distributed architectures. Instead of generating types per service, future tools will generate a single unified type system that spans all subgraphs, with automatic resolution of shared types and cross-service references. This eliminates the manual coordination required when services share types today.

Integration with AI-powered development tools is also emerging. Codegen plugins that generate documentation, suggest query optimizations, and automatically fix schema violations based on usage patterns are in development. These tools will analyze your codebase to identify unused fields, suggest fragment consolidation, and recommend schema improvements based on how your client code actually uses the API.

Performance Optimization with Code Generation

Code generation can also optimize GraphQL performance. Generate persisted query manifests that map query strings to hash IDs, allowing the server to reject unknown queries and reduce bandwidth by sending hashes instead of full query strings. Generate DataLoader factories from your schema that automatically batch and cache database queries, solving the N+1 problem that plagues naive GraphQL implementations. Generate response type validators that verify server responses match the expected schema at runtime, catching server-side bugs that would otherwise manifest as undefined behavior in the client. These generated utilities combine type safety with runtime safety, ensuring that your application handles unexpected data gracefully.

Migration and Adoption Strategy

Adopting GraphQL code generation in an existing project requires a phased approach. Start by adding code generation for your most critical queries, those used in your most important user flows. Generate types for these queries first and update the consuming components to use the generated types. This incremental approach lets you experience the benefits without a large upfront investment. As your team becomes comfortable with the generated types, expand code generation to cover all queries and mutations. Remove manual type definitions that are now redundant. Update your development workflow to regenerate types automatically when the schema changes, using file watchers or pre-commit hooks. Document the code generation setup so new team members can understand the workflow. The migration typically takes one to two sprints for a medium-sized codebase and pays for itself through reduced type-related bugs and faster feature development.

Conclusion

GraphQL Code Generator transforms how TypeScript developers work with GraphQL by eliminating manual type definitions and ensuring compile-time safety across your entire API layer. The key takeaways are:

  1. Set up codegen early in your projectβ€”it pays dividends immediately
  2. Use fragment colocation for scalable type management
  3. Integrate codegen into your CI pipeline with --check mode
  4. Leverage the plugin system to generate exactly the artifacts you need
  5. Use watch mode during development for instant type updates

Start with the client preset for the simplest setup, then customize as your needs grow. Once your team experiences fully-typed GraphQL operations, going back to manually typing API responses becomes unthinkable.