TechLead
Lesson 20 of 20
5 min read
GraphQL

GraphQL Code Generation

Automate TypeScript type generation from GraphQL schemas and operations for full end-to-end type safety

Why Code Generation?

One of GraphQL's greatest strengths is its strong type system, but without code generation, you end up manually writing TypeScript types that mirror your schema — a tedious, error-prone process that falls out of sync. GraphQL Code Generator automatically generates TypeScript types, hooks, and utilities from your schema and queries.

What Code Generation Provides

  • TypeScript types from schema: Generate interfaces for all your GraphQL types automatically
  • Typed operations: Generate exact types for each query and mutation result
  • Typed React hooks: Generate custom hooks like useGetUserQuery with full type safety
  • Resolver types: Generate resolver type signatures for the server
  • Compile-time validation: Catch type errors at build time, not runtime

Setup

# Install GraphQL Code Generator and plugins
npm install -D @graphql-codegen/cli
npm install -D @graphql-codegen/typescript
npm install -D @graphql-codegen/typescript-operations
npm install -D @graphql-codegen/typescript-react-apollo
npm install -D @graphql-codegen/typescript-resolvers

# Initialize configuration
npx graphql-code-generator init

Configuration File

// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';

const config: CodegenConfig = {
  // Schema source — can be URL, file, or glob
  schema: 'http://localhost:4000/graphql',

  // Document source — your .graphql files or inline gql tags
  documents: ['src/**/*.graphql', 'src/**/*.tsx'],

  generates: {
    // Client-side types and hooks
    'src/generated/graphql.ts': {
      plugins: [
        'typescript',                 // Base TypeScript types
        'typescript-operations',      // Types for queries/mutations
        'typescript-react-apollo',    // React hooks
      ],
      config: {
        withHooks: true,
        withHOC: false,
        withComponent: false,
        scalars: {
          DateTime: 'string',
          JSON: 'Record<string, unknown>',
        },
        enumsAsTypes: true,
        avoidOptionals: {
          field: true,
          inputValue: false,
          object: true,
        },
      },
    },

    // Server-side resolver types
    'src/generated/resolvers-types.ts': {
      plugins: ['typescript', 'typescript-resolvers'],
      config: {
        contextType: '../context#Context',
        mapperTypeSuffix: 'Model',
        mappers: {
          User: '../models#UserModel',
          Post: '../models#PostModel',
        },
      },
    },
  },

  // Ignore patterns
  ignoreNoDocuments: true,
};

export default config;

Writing GraphQL Operations

# src/graphql/queries.graphql

query GetUser($id: ID!) {
  user(id: $id) {
    id
    name
    email
    bio
    avatarUrl
    posts {
      id
      title
      publishedAt
    }
  }
}

query GetPosts($limit: Int!, $offset: Int, $filter: PostFilterInput) {
  posts(limit: $limit, offset: $offset, filter: $filter) {
    id
    title
    excerpt
    slug
    isPublished
    author {
      id
      name
      avatarUrl
    }
    publishedAt
    likesCount
  }
}

# src/graphql/mutations.graphql

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
    title
    slug
    isPublished
    createdAt
  }
}

mutation UpdatePost($id: ID!, $input: UpdatePostInput!) {
  updatePost(id: $id, input: $input) {
    id
    title
    body
    isPublished
    updatedAt
  }
}

mutation DeletePost($id: ID!) {
  deletePost(id: $id)
}

Running Code Generation

# Generate types
npx graphql-codegen

# Watch mode for development
npx graphql-codegen --watch

# Add to package.json scripts
# "codegen": "graphql-codegen",
# "codegen:watch": "graphql-codegen --watch"

Using Generated Types in React

// The generated file exports typed hooks and types
// src/generated/graphql.ts (auto-generated)
// export type User = { id: string; name: string; email: string; ... }
// export type GetUserQuery = { user: { id: string; name: string; ... } | null }
// export type GetUserQueryVariables = { id: string }
// export function useGetUserQuery(options: ...) { ... }

// Using generated hooks in components
import {
  useGetUserQuery,
  useGetPostsQuery,
  useCreatePostMutation,
  GetPostsQueryVariables,
  PostFilterInput,
} from '@/generated/graphql';

function UserProfile({ userId }: { userId: string }) {
  // Fully typed — data, loading, error are all correctly typed
  const { data, loading, error } = useGetUserQuery({
    variables: { id: userId }, // TypeScript ensures id is a string
  });

  if (loading) return <Skeleton />;
  if (error) return <ErrorMessage error={error} />;

  // data.user is typed — autocomplete works on all fields
  const user = data?.user;
  if (!user) return <p>User not found</p>;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <p>{user.bio}</p>
      {user.posts.map(post => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

function PostListPage() {
  const [filter, setFilter] = useState<PostFilterInput>({});

  const { data, loading, fetchMore } = useGetPostsQuery({
    variables: { limit: 10, offset: 0, filter },
  });

  return (
    <div>
      {data?.posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>By {post.author.name}</p>
          <span>{post.likesCount} likes</span>
        </article>
      ))}
    </div>
  );
}

function CreatePostForm() {
  const [createPost, { loading }] = useCreatePostMutation({
    // TypeScript ensures the input matches CreatePostInput
    onCompleted: (data) => {
      // data.createPost is fully typed
      console.log('Created:', data.createPost.title);
    },
  });

  const handleSubmit = (title: string, body: string) => {
    createPost({
      variables: {
        input: { title, body, isPublished: true },
      },
    });
  };

  // ...
}

Server-Side Resolver Types

// Using generated resolver types on the server
import { Resolvers } from './generated/resolvers-types';

// Full type safety for all resolvers
const resolvers: Resolvers = {
  Query: {
    // TypeScript enforces correct argument types and return types
    user: async (_parent, args, ctx) => {
      // args.id is typed as string
      return ctx.prisma.user.findUnique({ where: { id: args.id } });
    },

    posts: async (_parent, args, ctx) => {
      // args.limit, args.offset, args.filter are all correctly typed
      return ctx.prisma.post.findMany({
        take: args.limit ?? 20,
        skip: args.offset ?? 0,
      });
    },
  },

  User: {
    // parent is typed as UserModel (from mappers config)
    posts: async (parent, _args, ctx) => {
      return ctx.prisma.post.findMany({
        where: { authorId: parent.id },
      });
    },
  },

  Mutation: {
    createPost: async (_parent, args, ctx) => {
      // args.input is typed as CreatePostInput
      return ctx.prisma.post.create({
        data: {
          title: args.input.title,
          body: args.input.body,
          authorId: ctx.currentUser!.id,
        },
      });
    },
  },
};

Code Generation Best Practices

  • Run in CI: Add codegen to your CI pipeline to ensure generated types are always up to date
  • Commit generated files: Check generated files into version control so they are available without running codegen
  • Watch mode in dev: Run codegen in watch mode during development for instant type updates
  • Use mappers: Map GraphQL types to your database models on the server side
  • Separate client/server output: Generate client types and server resolver types in separate files

Continue Learning