The most surprising thing about GraphQL code generation is that it doesn’t just generate boilerplate; it fundamentally changes how you think about your GraphQL API.

Let’s say you have a GraphQL schema like this:

type User {
  id: ID!
  name: String!
  email: String
}

type Query {
  user(id: ID!): User
}

And you’re writing a client-side application in TypeScript. Without graphql-codegen, you’d likely be writing something like this to fetch a user:

// Manual fetching
const GET_USER_QUERY = `
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

async function fetchUser(userId: string) {
  const response = await fetch('/graphql', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      query: GET_USER_QUERY,
      variables: { id: userId },
    }),
  });
  const data = await response.json();
  // Type assertion needed here, prone to errors
  return data.data.user as { id: string; name: string; email?: string | null };
}

Notice the manual string for the query, the type assertion (as ...), and the potential for runtime errors if the API changes or the assertion is wrong.

Now, let’s introduce graphql-codegen. First, you’ll need to install it and its relevant plugins. For a typical TypeScript client, you’d install:

npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations

Next, create a configuration file, typically codegen.yml:

schema: ./schema.graphql # Path to your GraphQL schema
generates:
  ./generated/graphql.ts: # Output file for generated code
    plugins:
      - typescript # Generates TypeScript types for your schema
      - typescript-operations # Generates types for your queries/mutations/subscriptions
    config:
      # Optional: customize generation
      # useTypeImports: true
      # avoidOptionals: true

With this setup, you run the codegen CLI:

npx graphql-codegen --config codegen.yml

This command reads your schema.graphql file and, based on the codegen.yml configuration, creates generated/graphql.ts. This file will contain meticulously typed representations of your schema and operations.

The generated/graphql.ts might look something like this (simplified):

// In generated/graphql.ts

export type Maybe<T> = T | null;
export type InputMaybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
export type MakeEmpty<T extends { [key: string]: unknown }, K extends string | number | symbol> = { [P in K]: never };
export type Incremental<T> = T | { [P in keyof T]?: T[P] };

// ... other generated utility types

export type User = {
  __typename?: 'User';
  id: Scalars['ID'];
  name: Scalars['String'];
  email?: Maybe<Scalars['String']>;
};

export type Query = {
  __typename?: 'Query';
  user?: Maybe<User>;
};

export type UserQueryVariables = Exact<{
  id: Scalars['ID'];
}>;

export type UserQuery = { __typename?: 'Query' } & {
  user?: Maybe<{ __typename?: 'User' } & Pick<User, 'id' | 'name' | 'email'>>;
};

Now, in your client code, you can import and use these generated types directly. You’d typically use a GraphQL client library like Apollo Client or urql, which integrate seamlessly with graphql-codegen.

Here’s how fetchUser would look with graphql-codegen and Apollo Client:

// Using Apollo Client and generated types
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
// Import generated types
import { UserQuery, UserQueryVariables } from './generated/graphql';

const GET_USER_QUERY = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

async function fetchUserWithCodegen(userId: string) {
  const client = new Apollo.ApolloClient({
    // ... Apollo Client configuration
  });

  const { data, error } = await client.query<UserQuery, UserQueryVariables>({
    query: GET_USER_QUERY,
    variables: { id: userId },
  });

  if (error) {
    console.error("GraphQL Error:", error);
    return null;
  }

  // 'data' is now strongly typed!
  return data.user;
}

The benefits are immediate:

  1. Type Safety: data.user is now User | null | undefined (based on the schema and generation config), and data.user.id is string. No more risky type assertions.
  2. Intellisense: Your IDE will provide autocompletion for queries, mutations, and the shape of your data.
  3. Refactoring Confidence: If you rename a field in your GraphQL schema, graphql-codegen will highlight all the places in your client code that need updating.
  4. Reduced Boilerplate: The generated types handle all the complexity of Maybe, InputMaybe, and other GraphQL concepts.

The config section in codegen.yml is where you fine-tune the output. For instance, useTypeImports: true generates import type {...} statements, which are more efficient for TypeScript. avoidOptionals: true would make all fields non-nullable if your schema uses ! but you want to treat them as required in your code.

The real power comes from understanding that graphql-codegen isn’t just a formatter; it’s a way to enforce a contract between your client and server. Every time you run graphql-codegen, you’re validating that your client-side code is in sync with the server’s schema.

The most counterintuitive aspect is how much it simplifies complex GraphQL patterns like fragments and unions. You define a fragment once, and graphql-codegen generates a strongly typed object for that fragment, which you can then compose with your queries. This makes handling polymorphic data structures and shared data fetching logic incredibly robust.

The next logical step is to explore server-side generation with plugins like @graphql-codegen/typescript-resolvers to ensure your server implementation also adheres to the generated types, creating a fully typed GraphQL ecosystem.

Want structured learning?

Take the full Graphql-tools course →