A GraphQL schema is already a type system, so writing the same types again by hand in TypeScript is wasted effort and a source of drift. GraphQL Code Generator reads the schema and your operations and emits TypeScript for both sides. After this lesson you will be able to generate typed resolver signatures for a server, typed operation documents for a React client, and run generation as part of your workflow.
Install the CLI and the plugins for the outputs you need. The client preset bundles everything a frontend requires:
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-resolvers @graphql-codegen/client-presetConfiguration lives in codegen.ts at the project root:
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "./src/schema.graphql",
documents: ["./web/src/**/*.tsx"],
generates: {
"./src/generated/resolvers.ts": {
plugins: ["typescript", "typescript-resolvers"],
config: { contextType: "../context#Context", useIndexSignature: true },
},
"./web/src/gql/": { preset: "client" },
},
ignoreNoDocuments: true,
};
export default config;schema can be a local SDL file, a glob, or the URL of a running server with introspection enabled. Run npx graphql-codegen once, or npx graphql-codegen --watch during development, and add it to package.json scripts so it runs before build.
The typescript-resolvers plugin produces a Resolvers type that mirrors the schema. Annotating the resolver map with it makes the compiler check every field, argument and return value:
import type { Resolvers } from "./generated/resolvers";
export const resolvers: Resolvers = {
Query: {
post: (_parent, { id }, { db }) => db.posts.find(id),
},
Post: {
author: (post, _args, { loaders }) => loaders.userById.load(post.authorId),
},
};{ id } is typed from the schema argument, { db } from the contextType you configured, and returning a Post with a missing non-null field is a compile error. Resolvers often return database rows that differ from the GraphQL type (a Post row has authorId, the schema has author). The mappers option maps a GraphQL type to the TypeScript type your resolvers actually return, for example mappers: { Post: "../db#PostRow" }, so the parent argument of Post.author is typed as the row.
The client preset generates a graphql() function. Wrap every operation in it instead of gql, and the returned document carries its result and variable types:
import { graphql } from "./gql";
import { useQuery } from "@apollo/client/react";
const GET_POST = graphql(`
query GetPost($id: ID!) {
post(id: $id) { id title author { name } }
}
`);
export function PostView({ id }: { id: string }) {
const { data } = useQuery(GET_POST, { variables: { id } });
return <h1>{data?.post?.title}</h1>; // data.post is fully typed, including nullability
}The document is a TypedDocumentNode, a standard understood by Apollo Client, urql and graphql-request, so no plugin-specific hooks are needed. Passing a wrong variable type or reading a field that the operation did not select fails at compile time. Fragments generated by the preset are masked by default: a component that spreads PostCard fragment must unwrap it with useFragment from the generated folder, which enforces the same data-ownership discipline Relay promotes.
printSchema so codegen can read it.documents glob should match only files that contain operations; ignoreNoDocuments: true prevents failures in server-only packages.What does annotating the resolver map with the generated `Resolvers` type give you?
typescript-resolvers produces a Resolvers type; contextType and mappers align it with your context and database rows.client preset's graphql() function yields TypedDocumentNode documents that type useQuery and useMutation results.Next lesson: GraphQL with Next.js: Server Components and Route Handlers — query a GraphQL API from Server Components, host one in a Route Handler, and use Apollo in Client Components.