41.2 GraphQL Code Generator: Producing Types from Your Schema
Right, let’s get this party started. You’ve got a GraphQL schema, you’ve got some frontend code, and you’re tired of playing the human type-checker, manually writing interfaces that describe what your API returns. It’s tedious, error-prone, and frankly, a little insulting to your intelligence. You’re a programmer; you make machines do the boring work.
This is where GraphQL Code Generator (graphql-codegen) comes in. Think of it as your own personal, hyper-competent intern who reads your entire GraphQL schema and API operations, then generates perfect, type-safe TypeScript definitions for all of it. It connects the dots between your backend’s schema and your frontend’s expectations, and it does it with a level of precision no caffeine-deprived human can match on a Friday afternoon.
The Core Setup: Making the Magic Happen
First, you need to install the thing. It’s a CLI tool and a constellation of plugins, so we grab the core and the ones we need.
npm install -D @graphql-codegen/cli
npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-operations
Next, you need to tell this intern what its job is. You create a codegen.ts (or .js or .yaml) file. I prefer TypeScript because, well, we’re in a TypeScript chapter.
// codegen.ts
import type { CodegenConfig } from '@graphql-codegen/cli';
const config: CodegenConfig = {
schema: 'http://localhost:4000/graphql', // Or path to a schema.graphql file
documents: ['src/**/*.tsx', 'src/**/*.ts'], // Where your queries live
generates: {
'./src/gql/': { // Where to put the output
preset: 'client',
plugins: [],
},
},
};
export default config;
The preset: 'client' is the modern, blessed way. It’s a preset that bundles a bunch of sensible plugins and gives you a nice client-friendly output. Then, you add a script to your package.json:
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch"
}
Run npm run codegen. If the gods of GraphQL are smiling upon you, you’ll now have a shiny new src/gql/ directory filled with types. The first time you see it, it feels like magic. The hundredth time, it just feels like correct.
How to Actually Use It Without Losing Your Mind
The generated output from the client preset gives you a gql function and strongly typed hooks. Here’s how it works in practice. Let’s say you have a query:
// In some component file, e.g., src/components/UserProfile.tsx
import { graphql } from '../gql'; // This is the generated folder!
// Write your query. This is NOT a string. It's using the generated `gql` function.
const GetUserDocument = graphql(`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
posts {
title
}
}
}
`);
Now, because you imported your gql function from the generated library, the Code Generator is aware of this query. The next time you run codegen, it will see this query, analyze it against the schema, and generate a perfect type for its result.
You use the generated hook, which is named based on your operation. The pattern is use<OperationName>Query.
import { useGetUserQuery } from '../gql'; // Yep, also generated!
const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
// This hook is now fully typed. `data` is { user: { id: string, name: string, ... } } | null | undefined
// `variables` are typed to expect `{ id: string }`
const { data, loading, error } = useGetUserQuery({
variables: { id: userId },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error! {error.message}</p>;
// TypeScript KNOWS this data is now defined if we're here, and knows the exact shape.
return (
<div>
<h1>{data.user.name}</h1>
<p>{data.user.email}</p>
<h2>Posts:</h2>
<ul>
{data.user.posts.map((post) => (
<li key={post.title}>{post.title}</li> // Try typing `post.` and enjoy the autocomplete
))}
</ul>
</div>
);
};
The beauty is in the autocomplete and the safety. Try to access data.user.socialSecurityNumber? TypeScript error. Pass a number instead of a string for the id variable? TypeScript error. Your editor becomes a guard rail that prevents you from making requests that can’t possibly work.
Common Pitfalls and the “Oh Crap” Moments
This all sounds perfect, until it isn’t. Here’s where the seams show.
The Schema Mismatch: The most common “oh crap” moment is when your backend schema changes and you forget to re-run the codegen. Your frontend now has stale types and you’ll only find out at runtime. This is why the --watch script is your best friend during development, and why you should absolutely run codegen as part of your CI/CD build process. If the schema has changed, the build should fail until the types are regenerated and the resulting type errors are fixed.
The Nullability Trap: GraphQL’s type system is all about nullability. A field String is nullable. A field String! is non-nullable. Codegen mirrors this exactly. This is a good thing, but it can be a surprise. Your data.user.name might be string | null | undefined. The undefined is from the React Hook itself before the query completes, the null is from GraphQL. You must handle these states. This isn’t a flaw of the generator; it’s it correctly telling you the uncomfortable truth about your data.
Custom Scalars: GraphQL has a DateTime scalar? Great. TypeScript doesn’t. By default, Codegen will just type this as any or string, which is useless. You have to tell it what to do. This is a critical config you must not overlook.
// In your codegen.ts
const config: CodegenConfig = {
// ... other config
config: {
scalars: {
DateTime: 'string', // Or 'Date' if you're actually parsing it into Date objects
JSON: '{ [key: string]: any }',
},
},
};
This is the part where you stop being a mere user of the tools and start actually designing the contract between your frontend and backend. It’s the most powerful part of the whole setup. You’re not just generating types; you’re formally defining how your application understands data. And that, my friend, is a whole different level of engineering.