GraphQL Type Generation Codegen: A Practical, End-to-End Guide

Generate GraphQL types for clients and servers with codegen. Learn configs, patterns, and pitfalls for safer APIs and faster delivery.

ASOasis
7 min read
GraphQL Type Generation Codegen: A Practical, End-to-End Guide

Image used for representation purposes only.

Overview

GraphQL type generation (“codegen”) turns your schema and operations into strongly typed code for clients and servers. The payoff is big: fewer runtime errors, safer refactors, smarter IDE autocompletion, and faster teams. This guide explains the why, what, and how of GraphQL codegen, with practical configurations, examples, and patterns you can apply in real projects.

Why generate types at all?

  • Single source of truth: Your GraphQL schema defines the contract. Codegen mirrors it as types so your app and services never drift.
  • Safer development: Mistyped field names, missing variables, or incorrect nullability are caught at compile time.
  • Faster DX: Editors autocomplete fields, variables, and fragments. Refactors propagate automatically.
  • Leaner runtime checks: Let the type system carry load you might otherwise handle with ad‑hoc validation.

What gets generated?

  • Schema types: The object, enum, union, interface, and scalar shapes from your schema.
  • Operation types: Exact shapes for queries/mutations/subscriptions and their variables.
  • Resolver types: Signatures and parent/args/context info for server resolvers.
  • Typed documents: DocumentNodes or framework-specific hooks wired to your operations.

Tooling landscape at a glance

  • GraphQL Code Generator (by The Guild): A plugin-based powerhouse for TypeScript and more; generates client and server types, typed DocumentNodes, React hooks, URQL helpers, and resolver signatures.
  • Apollo Client + ecosystem: Works great with typed DocumentNodes or framework-specific codegen plugins; Apollo Kotlin/iOS provide native mobile codegen.
  • Relay: A batteries-included approach with compile-time guarantees and fragment-driven architecture (Flow/TypeScript).
  • Code-first frameworks (e.g., Nexus, Pothos, TypeGraphQL): Generate a schema from code, usually exposing types directly in TypeScript; often paired with client-side codegen.

Use what fits your stack; many teams pair a code-first server with client-side operation codegen for end-to-end safety.

A practical workflow with GraphQL Code Generator (TypeScript)

Below is a minimal, production-friendly setup covering both client and server.

1) Define a schema (SDL)

# schema.graphql
scalar DateTime

type Post {
  id: ID!
  title: String!
  body: String!
  author: User!
  publishedAt: DateTime
}

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

type Query {
  post(id: ID!): Post
  posts(limit: Int = 20): [Post!]!
}

type Mutation {
  publishPost(id: ID!): Post!
}

2) Write operations and fragments

# src/operations/post.graphql
fragment PostCard on Post {
  id
  title
  author { id name }
}

query GetPost($id: ID!) {
  post(id: $id) {
    ...PostCard
    body
    publishedAt
  }
}

mutation PublishPost($id: ID!) {
  publishPost(id: $id) { id title }
}

3) Configure codegen for client and server

# codegen.yml
schema: ./schema.graphql
documents: ./src/**/*.graphql

generates:
  # Client preset: typed DocumentNodes, fragment masking helpers, and a typed `gql` tag
  ./src/gql/:
    preset: client
    config:
      defaultScalarType: unknown
      scalars:
        DateTime: string

  # Server: resolver signatures wired to your Context and backend models
  ./src/__generated__/resolvers-types.ts:
    plugins:
      - typescript
      - typescript-resolvers
    config:
      useTypeImports: true
      contextType: '../context#Context'
      defaultScalarType: unknown
      scalars:
        DateTime: string
      mappers:
        # Map GraphQL types to your domain models (e.g., Prisma)
        Post: '@prisma/client#Post'
        User: '@prisma/client#User'

Run it:

npx graphql-codegen

4) Use the generated client types

// src/app/Post.tsx
import { useQuery } from '@apollo/client'
import { GetPostDocument } from '../gql'

export function Post({ id }: { id: string }) {
  const { data, loading, error } = useQuery(GetPostDocument, { variables: { id } })
  if (loading) return <p>Loading…</p>
  if (error) return <p>Error: {error.message}</p>
  if (!data?.post) return <p>Missing post</p>

  // `data.post` is fully typed, including nullable fields
  return (
    <article>
      <h1>{data.post.title}</h1>
      <p>by {data.post.author.name}</p>
      <time>{data.post.publishedAt ?? 'Draft'}</time>
      <div>{data.post.body}</div>
    </article>
  )
}

Prefer framework-agnostic typed documents? Use any client with the same safety:

// src/app/useGetPost.ts
import { GetPostDocument } from '../gql'
import { createClient } from 'urql'

const client = createClient({ url: '/graphql' })
export const fetchPost = (id: string) => client.query(GetPostDocument, { id }).toPromise()

5) Implement resolvers with types

// src/resolvers.ts
import type { Resolvers } from './__generated__/resolvers-types'

export const resolvers: Resolvers = {
  Query: {
    post: (_p, { id }, { db }) => db.post.findUnique({ where: { id } }),
    posts: (_p, { limit = 20 }, { db }) => db.post.findMany({ take: limit }),
  },
  Mutation: {
    publishPost: async (_p, { id }, { db }) => {
      return db.post.update({ where: { id }, data: { publishedAt: new Date() } })
    },
  },
  Post: {
    author: (post, _args, { db }) => db.user.findUnique({ where: { id: post.authorId } }),
  },
}

Note how argument, context, and return types are inferred from the schema and your mappers.

Advanced patterns you’ll actually use

Fragment masking and co-location

Keep UI components honest by selecting only the fields they render. With the client preset, you get helpers that force access through fragment spreads, preventing accidental field usage that wasn’t queried.

Custom scalars (DateTime, JSON, URL)

Map scalars to precise runtime types to avoid any creeping in. In codegen.yml, set scalars to rich types (e.g., branded strings) or domain types.

config:
  scalars:
    DateTime: string
    JSON: 'Record<string, unknown>'
    URL: string

Nullability rules that match reality

GraphQL’s X vs X! is not the same as TypeScript’s optional. Consider turning on exactOptionalPropertyTypes in tsconfig.json and be explicit about null vs undefined in your app logic.

Mapping GraphQL types to domain models

Use mappers so your resolver parents are typed as your ORM entities (e.g., Prisma). This removes a whole class of type assertions and narrows result shapes.

Persisted operations

Bake operation IDs at build time for safer, cacheable requests and smaller payloads. Many setups generate a manifest mapping operation names to hashes. Pair this with your gateway or CDN to reject unknown operations.

Federation and subgraphs

Generate resolver types for each subgraph, and operation types for gateway-level queries. Keep subgraph schemas versioned and run codegen per package to avoid cross-service drift.

Monorepos at scale

  • Generate into package-local __generated__ folders and re-export stable interfaces.
  • Use task runners (e.g., Turbo/Nx) with cache keys on schema.graphql and src/**/*.graphql.
  • Run codegen in --watch mode during dev to keep types fresh.

Testing and CI

  • Schema diffs: Detect breaking changes automatically.
  • Operation coverage: Ensure every operation compiles against the latest schema.
  • Generated code check-in: Commit generated code when it speeds CI and improves local DX; otherwise, validate with a “clean tree” check that generation produces no diffs.

Example CI snippets:

# Verify types are current
npx graphql-codegen --errors-only

# Detect breaking schema changes (baseline vs PR)
# graphql-inspector or similar tools
npx graphql-inspector diff schema.old.graphql schema.graphql

Common pitfalls and how to avoid them

  • Stale schema: Pin your schema source (SDL file, remote URL, or introspection JSON) and update deterministically in CI.
  • Any by accident: Set defaultScalarType: never | unknown to flush out missing mappings.
  • Over-fetching: Lean on fragments; avoid component access to fields it didn’t query (fragment masking helps).
  • Optional vs nullable confusion: Treat null as a first-class citizen in UI and API logic; don’t conflate with undefined.
  • Leaky __typename: Keep it in documents (helps caching), but avoid displaying it casually in UI.
  • Untyped context: Always type your GraphQL context and propagate it into resolver types for auth, loaders, and telemetry.

Code-first servers still benefit from codegen

Even if your server is code-first, your clients aren’t. Export SDL (or use introspection) and generate client operation types. You can also generate typed DocumentNodes from your code-first schema to use in server-to-server requests or integration tests.

Performance notes

  • Compile-time guarantees reduce runtime guards and dead code.
  • Typed fragments enable finer-grained memoization in UI components.
  • Persisted operations shrink network payloads and improve cache hit rates.

A pragmatic checklist

  • Schema is version-controlled and reproducible.
  • Codegen runs in watch mode locally and in CI on every change.
  • Scalars are fully mapped; no stray any.
  • Resolver parents/returns/context are typed via mappers.
  • Client uses typed DocumentNodes (or framework-specific hooks) and fragments.
  • CI checks for stale generation and breaking schema diffs.
  • Optional vs nullable semantics are consistent across TS and UI.

Conclusion

GraphQL codegen turns your schema into a force multiplier for reliability and velocity. Start with a minimal client preset for typed documents, add resolver types on the server, and grow into advanced patterns like fragment masking, mappers, and persisted operations. The result is a codebase that refactors with confidence, fails fast at compile time, and ships features faster.

Related Posts