GraphQL Code Generation: A Practical Client Tutorial with TypeScript and React
A step-by-step tutorial to generate typed GraphQL client code and React hooks with TypeScript, Apollo, or urql—plus scalars, fragments, and CI.
Image used for representation purposes only.
Overview
GraphQL code generation turns your schema and queries into strongly typed client code—saving you from hand‑writing TypeScript interfaces, React hooks, and boilerplate request helpers. In this tutorial, you’ll set up a codegen workflow, generate types and hooks, wire them into Apollo Client or urql, handle custom scalars, and automate the flow in watch mode and CI. Everything is incremental, so you can adopt it in an existing app without a rewrite.
What you’ll build
- A TypeScript React client that queries a GraphQL API
- Auto‑generated, strongly typed operations and React hooks
- A repeatable codegen pipeline you can run locally, in watch mode, and in CI
You can substitute your own schema/endpoint. The examples assume a Books API with types like Book, Author, and scalars like DateTime.
Prerequisites
- Node.js 18+ and npm (or pnpm/yarn)
- Basic familiarity with GraphQL operations (query/mutation) and React
- A running GraphQL endpoint or a local SDL schema file
Step 1: Choose a runtime client
GraphQL codegen is client‑agnostic. Common choices:
- Apollo Client: Full‑featured cache, React hooks.
- urql: Lightweight, modular, hooks‑first.
- Plain fetch or graphql-request: Minimal runtime surface; great for SSR, workers, or Node services.
We’ll demonstrate Apollo and urql; the generated artifacts are shared.
Step 2: Install GraphQL Code Generator
The Guild’s GraphQL Code Generator is the de‑facto tool. Install the CLI and the modern “client preset,” which generates typed DocumentNodes and helpers without lock‑in to a single client.
# App dependencies (choose one runtime client)
npm i @apollo/client graphql
# or
npm i urql graphql
# Dev dependencies
npm i -D @graphql-codegen/cli @graphql-codegen/client-preset
Why the client preset?
- It generates a zero‑runtime typed graphql helper and typed documents.
- Works with Apollo, urql, TanStack Query + graphql-request, and custom fetch.
- Encourages colocated .graphql documents, fragments, and operations.
Step 3: Point codegen at your schema and documents
Create a TypeScript config for codegen at the project root. You can use YAML if you prefer; TS offers type‑safe autocomplete.
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.{ts,tsx,graphql,gql}'],
generates: {
'./src/gql/': {
preset: 'client',
config: {
// Map custom scalars to TS types
scalars: {
DateTime: 'string',
URL: 'string',
JSON: 'Record<string, unknown>'
}
}
}
}
};
export default config;
Notes:
- schema can be a URL, a glob of SDL files, or multiple entries.
- documents points to .graphql files and embedded gql strings in .ts/.tsx.
- The ./src/gql folder will contain the generated graphql() helper, typed fragments, and documents.
Step 4: Write your first operation
Create a colocated GraphQL document next to a component.
# src/features/books/queries.graphql
fragment BookCard on Book {
id
title
publishedAt
author { id name }
}
query GetBooks($limit: Int!) {
books(limit: $limit) {
...BookCard
}
}
Step 5: Generate code
Add scripts, then run codegen.
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
npm run codegen
You’ll get:
- src/gql/graphql.ts: the typed graphql() function and type exports
- src/gql/fragment-masking.ts: fragment utilities
- src/gql/ (operation documents compiled to typed DocumentNode)
Step 6: Use with Apollo Client
Set up Apollo normally, then import the typed document and pass variables with full type safety.
// src/app/apollo.tsx
import { ApolloClient, InMemoryCache, ApolloProvider, HttpLink } from '@apollo/client';
export const client = new ApolloClient({
link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
cache: new InMemoryCache()
});
// src/features/books/BooksList.tsx
import { useQuery } from '@apollo/client';
import { graphql } from '../../gql';
const GetBooksDocument = graphql(/* GraphQL */ `
query GetBooks($limit: Int!) {
books(limit: $limit) {
id
title
publishedAt
author { id name }
}
}
`);
export function BooksList() {
const { data, loading, error } = useQuery(GetBooksDocument, { variables: { limit: 10 } });
if (loading) return <p>Loading…</p>;
if (error) return <p role="alert">{error.message}</p>;
return (
<ul>
{data?.books?.map(b => (
<li key={b.id}>
<strong>{b.title}</strong> — {b.author?.name}
</li>
))}
</ul>
);
}
Notice you never hand‑write types. The variables and response types are inferred from the generated DocumentNode.
Step 7: Use with urql
Switching runtime clients does not change your documents.
// src/app/urql.tsx
import { createClient, Provider } from 'urql';
export const client = createClient({ url: 'http://localhost:4000/graphql' });
// src/features/books/BooksListUrql.tsx
import { useQuery } from 'urql';
import { graphql } from '../../gql';
const GetBooksDocument = graphql(/* GraphQL */ `
query GetBooks($limit: Int!) {
books(limit: $limit) { id title author { name } }
}
`);
export function BooksListUrql() {
const [{ data, fetching, error }] = useQuery({ query: GetBooksDocument, variables: { limit: 5 } });
if (fetching) return <p>Loading…</p>;
if (error) return <p role="alert">{error.message}</p>;
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
Step 8: Fragments and type‑safety patterns
Fragments encourage reuse and unlock precise type‑narrowing.
# src/features/books/fragments.graphql
fragment AuthorName on Author { id name }
fragment BookCard on Book {
id
title
author { ...AuthorName }
}
# src/features/books/bookById.graphql
query BookById($id: ID!) {
book(id: $id) {
...BookCard
... on Textbook { gradeLevel }
... on Novel { genre }
}
}
Inside React, unions and interfaces are reflected in TS discriminated unions. Inline fragments (… on Type) narrow types when you check __typename.
if (book.__typename === 'Textbook') {
// book.gradeLevel is defined here
}
Step 9: Mutations and optimistic UI
Mutations work the same way—typed variables in, typed data out.
# src/features/books/mutations.graphql
mutation AddBook($input: AddBookInput!) {
addBook(input: $input) {
id
title
author { id name }
}
}
import { useMutation } from '@apollo/client';
import { graphql } from '../../gql';
const AddBookDocument = graphql(/* GraphQL */ `
mutation AddBook($input: AddBookInput!) {
addBook(input: $input) { id title author { id name } }
}
`);
function AddBookForm() {
const [addBook] = useMutation(AddBookDocument);
// variables are fully typed
return (
<button
onClick={() =>
addBook({
variables: { input: { title: 'New', authorId: '1' } },
optimisticResponse: {
addBook: { id: 'temp', title: 'New', author: { id: '1', name: 'TBD', __typename: 'Author' }, __typename: 'Book' }
}
})
}
>Add</button>
);
}
Step 10: Custom scalars and runtime validation
You mapped DateTime/URL/JSON to TypeScript in the config. If you need runtime safety (e.g., to assert URL is valid), introduce a branded type or zod/io‑ts validation at your network boundary.
Example with a branded URL string:
// src/types/url.ts
export type UrlString = string & { readonly __brand: unique symbol };
export function asUrlString(value: string): UrlString {
new URL(value); // throws if invalid
return value as UrlString;
}
Update codegen mapping:
config: {
scalars: { URL: 'import("./src/types/url").UrlString' }
}
Now fields with scalar URL are typed as UrlString.
Step 11: Watch mode and pre‑commit safety
- Development: npm run codegen:watch keeps types in sync while you edit documents.
- Type safety: add tsc –noEmit to your test or build pipeline so invalid variable shapes fail fast.
- Linting: ensure your .eslintignore includes generated folders or configure rules for generated files.
Step 12: Testing with MSW GraphQL
Mock at the network boundary while retaining generated types.
// src/test/handlers.ts
import { graphql as mswGraphql } from 'msw';
export const handlers = [
mswGraphql.query('GetBooks', (req, res, ctx) => {
return res(ctx.data({ books: [{ id: '1', title: 'Mocked', author: { id: 'a1', name: 'Alice' } }] }));
})
];
Because the DocumentNode is named, MSW can match on operation name (GetBooks) without brittle string selectors.
Advanced: Near‑operation files and client‑specific hooks
Prefer client‑preset for portability. If you want client‑specific hooks generated near each document, use the classic plugin stack.
// codegen.ts (alternative)
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql',
documents: ['src/**/*.{ts,tsx,graphql}'],
generates: {
'./src/graphql-types.ts': {
plugins: ['typescript', 'typescript-operations']
},
'./src/': {
preset: 'near-operation-file',
presetConfig: {
extension: '.generated.tsx',
baseTypesPath: 'graphql-types.ts'
},
plugins: ['typescript-react-apollo']
}
}
};
export default config;
This yields useGetBooksQuery and friends next to the source document. Swap typescript-react-apollo for typescript-urql to target urql.
Troubleshooting checklist
- Operation not found: Is the file matched by documents globs? Did you save before running codegen?
- Duplicate operation name: Names must be unique across documents; keep them consistent with component names.
- Schema conflicts: Clear caches, ensure the schema URL is reachable, or switch to a local SDL file while offline.
- Any/unknown types: Verify scalar mappings and that your fragments/inline fragments target the correct concrete types.
Performance and DX tips
- Keep operations small and colocated; re‑use fragments for shared UI pieces.
- Use fragment masking (enabled by the client preset) to avoid accidentally reading fields you didn’t query.
- Generate in CI and check in artifacts for libraries/SDKs; skip committing in app repos if churn is high.
- Consider persisted queries for bandwidth wins in production builds.
Summary
With a few lines of configuration, GraphQL code generation removes an entire class of runtime bugs. You author plain GraphQL next to your components; the tool produces fully typed documents and helpers that integrate cleanly with Apollo, urql, or plain fetch. Start with the client preset for portability, add scalar mappings as you go, and let watch mode keep your types evergreen while you ship features faster and safer.
Related Posts
Build a GraphQL API and React Client: An End‑to‑End Tutorial
Learn GraphQL with React and Apollo Client by building a full stack app with queries, mutations, caching, and pagination—step by step.
Building an Accessible, Responsive React Collapsible Sidebar Navigation
Build a fast, accessible, responsive React collapsible sidebar with TypeScript, ARIA, keyboard support, and persisted state—no UI library required.
Build a Production‑Ready React Avatar User Profile Component
Build an accessible, flexible React avatar component with initials fallback, status badges, groups, and TypeScript—optimized for performance and a11y.