GraphQL Code Generator: Typed Hooks, Schema-First Development, and Client Presets
Set up GraphQL Code Generator for production: generate fully-typed React Query hooks from your schema, configure client presets for fragment colocation, implement persisted operations for performance, and integrate into CI/CD.
GraphQL without code generation is a pain: you write a query, then manually write TypeScript types for the response, and they drift apart the moment someone changes the schema. GraphQL Code Generator eliminates the drift by generating types and React Query hooks directly from your .graphql files โ the types are always correct because they come from the same schema your server uses.
The 2026 setup: client preset for the frontend (colocated fragments, typed document nodes), typescript-resolvers for the backend (typed resolver functions), and persisted operations for production performance.
Installation and Base Config
npm install -D @graphql-codegen/cli @graphql-codegen/client-preset
npm install @graphql-typed-document-node/core
# For backend resolver types
npm install -D @graphql-codegen/typescript @graphql-codegen/typescript-resolvers
// codegen.ts โ TypeScript config (preferred over codegen.yml)
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "http://localhost:4000/graphql", // Or path to schema file
// Multiple schema sources can be merged:
// schema: ["./src/schema/**/*.graphql", "http://localhost:4000/graphql"]
generates: {
// Frontend: client preset generates typed document nodes + React Query hooks
"./src/gql/": {
documents: ["./src/**/*.tsx", "./src/**/*.ts"],
preset: "client",
presetConfig: {
// Fragment masking: components only access fields they declare
// Prevents accidental dependency on fields from other fragments
fragmentMasking: { unmaskFunctionName: "getFragmentData" },
// Generate persisted document IDs for performance
persistedDocuments: true,
},
plugins: [],
config: {
// Use React Query hooks
documentMode: "string",
// Strict scalars โ no implicit `any` for custom scalars
strictScalars: true,
scalars: {
DateTime: "string",
UUID: "string",
JSON: "Record<string, unknown>",
},
},
},
// Backend: typed resolver interfaces
"./src/schema/resolvers.generated.ts": {
plugins: ["typescript", "typescript-resolvers"],
config: {
contextType: "../context#Context",
mappers: {
User: "../models/user#UserModel",
Project: "../models/project#ProjectModel",
},
strictScalars: true,
scalars: {
DateTime: "string",
UUID: "string",
},
},
},
},
hooks: {
afterAllFileWrite: ["prettier --write"],
},
};
export default config;
// package.json
{
"scripts": {
"codegen": "graphql-codegen --config codegen.ts",
"codegen:watch": "graphql-codegen --config codegen.ts --watch"
}
}
Schema Definition
# src/schema/schema.graphql
scalar DateTime
scalar UUID
type Query {
user(id: UUID!): User
users(
first: Int = 20
after: String
filter: UserFilter
): UserConnection!
project(id: UUID!): Project
}
type Mutation {
createProject(input: CreateProjectInput!): CreateProjectPayload!
updateProject(id: UUID!, input: UpdateProjectInput!): UpdateProjectPayload!
deleteProject(id: UUID!): DeleteProjectPayload!
}
type Subscription {
projectUpdated(projectId: UUID!): ProjectUpdatedEvent!
}
type User {
id: UUID!
email: String!
name: String!
plan: Plan!
createdAt: DateTime!
projects(first: Int = 10): ProjectConnection!
}
type Project {
id: UUID!
name: String!
description: String
visibility: Visibility!
owner: User!
createdAt: DateTime!
updatedAt: DateTime!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
enum Plan { FREE STARTER GROWTH ENTERPRISE }
enum Visibility { PRIVATE TEAM PUBLIC }
๐ Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different โ senior engineers only, direct Slack access, and a 5.0โ Upwork record across 100+ projects.
- React, Next.js, Node.js, TypeScript โ production-grade stack
- Fixed-price contracts โ no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Writing Queries with Fragment Colocation
With the client preset, each component declares exactly the fields it needs via fragments โ no more over-fetching or accidental dependency on parent query fields:
# src/components/UserCard/UserCard.graphql
# Component declares what it needs
fragment UserCard_User on User {
id
name
email
plan
}
// src/components/UserCard/UserCard.tsx
import { getFragmentData } from "@/gql";
import { UserCard_UserFragment, UserCard_UserFragmentDoc } from "@/gql/graphql";
interface Props {
// Component accepts the fragment type โ not the full User type
user: UserCard_UserFragment | { " $fragmentRefs": { UserCard_UserFragment: UserCard_UserFragment } };
}
export function UserCard({ user: userProp }: Props) {
// Unmask the fragment โ this is where fragment masking is enforced
const user = getFragmentData(UserCard_UserFragmentDoc, userProp);
return (
<div className="border rounded p-4">
<h3 className="font-semibold">{user.name}</h3>
<p className="text-gray-500">{user.email}</p>
<span className="badge">{user.plan}</span>
</div>
);
}
# src/app/users/page.graphql
# Parent query spreads the component fragment
query GetUsers($first: Int, $after: String) {
users(first: $first, after: $after) {
edges {
node {
id
...UserCard_User # Spread the fragment
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
Generated Typed Hooks with React Query
// src/app/users/page.tsx
"use client";
import { useQuery } from "@tanstack/react-query";
import { graphqlRequest } from "@/lib/graphql-client";
import { GetUsersDocument, GetUsersQuery, GetUsersQueryVariables } from "@/gql/graphql";
import { UserCard } from "@/components/UserCard/UserCard";
// The generated hook wrapper โ typed end-to-end
function useGetUsers(variables: GetUsersQueryVariables) {
return useQuery({
queryKey: ["users", variables],
queryFn: () =>
graphqlRequest<GetUsersQuery, GetUsersQueryVariables>(
GetUsersDocument,
variables
),
});
}
export default function UsersPage() {
const { data, isLoading, error } = useGetUsers({ first: 20 });
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<ul>
{data?.users.edges.map(({ node }) => (
// TypeScript knows exactly what fields are available on node
<li key={node.id}>
<UserCard user={node} />
</li>
))}
</ul>
);
}
๐ Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4โ8 weeks, full platforms in 3โ5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
GraphQL Client Setup
// src/lib/graphql-client.ts
import { TypedDocumentNode } from "@graphql-typed-document-node/core";
import { print } from "graphql";
interface GraphQLResponse<T> {
data?: T;
errors?: Array<{ message: string; locations?: unknown; path?: unknown }>;
}
export async function graphqlRequest<TData, TVariables>(
document: TypedDocumentNode<TData, TVariables>,
variables?: TVariables,
options?: RequestInit
): Promise<TData> {
const response = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
...getAuthHeaders(),
...(options?.headers ?? {}),
},
body: JSON.stringify({
query: print(document),
variables,
}),
...options,
});
if (!response.ok) {
throw new Error(`GraphQL request failed: ${response.status} ${response.statusText}`);
}
const result: GraphQLResponse<TData> = await response.json();
if (result.errors?.length) {
throw new GraphQLError(result.errors);
}
if (!result.data) {
throw new Error("GraphQL response missing data");
}
return result.data;
}
// With persisted operations: send hash instead of full query text
export async function graphqlPersistedRequest<TData, TVariables>(
document: TypedDocumentNode<TData, TVariables>,
variables?: TVariables
): Promise<TData> {
// The client preset generates a hash for each document
const documentId = (document as any).__meta__?.hash;
const response = await fetch(process.env.NEXT_PUBLIC_GRAPHQL_URL!, {
method: "POST",
headers: {
"Content-Type": "application/json",
...getAuthHeaders(),
},
body: JSON.stringify({
extensions: {
persistedQuery: {
version: 1,
sha256Hash: documentId,
},
},
variables,
}),
});
const result: GraphQLResponse<TData> = await response.json();
// If server doesn't have the persisted query, send full query
if (result.errors?.some((e) => e.message === "PersistedQueryNotFound")) {
return graphqlRequest(document, variables);
}
if (result.errors?.length) throw new GraphQLError(result.errors);
return result.data!;
}
Backend: Typed Resolvers
// src/resolvers/user.resolver.ts
// The generated Resolvers type ensures every resolver is correctly typed
import { Resolvers } from "../schema/resolvers.generated";
import { UserModel } from "../models/user";
export const userResolvers: Resolvers = {
Query: {
user: async (_parent, { id }, { dataloaders }) => {
// Return type is inferred: UserModel | null
return dataloaders.user.load(id);
},
users: async (_parent, { first, after, filter }, { db }) => {
// TypeScript enforces the return shape matches UserConnection
const users = await db.query<UserModel>(/* ... */);
return {
edges: users.map((user) => ({ node: user, cursor: user.id })),
pageInfo: { hasNextPage: false, hasPreviousPage: false },
totalCount: users.length,
};
},
},
User: {
// Field resolvers โ TypeScript knows parent is UserModel
projects: async (parent, { first }, { dataloaders }) => {
const projects = await dataloaders.projectsByUser.load(parent.id);
return {
edges: projects.slice(0, first ?? 10).map((p) => ({ node: p })),
pageInfo: { hasNextPage: false, hasPreviousPage: false },
totalCount: projects.length,
};
},
},
};
CI Integration
# .github/workflows/graphql-codegen.yml
name: GraphQL Code Generation
on:
push:
paths:
- "src/schema/**/*.graphql"
- "src/**/*.graphql"
- "codegen.ts"
jobs:
codegen:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- name: Start GraphQL server (for schema introspection)
run: npm run dev &
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Wait for server
run: npx wait-on http://localhost:4000/graphql --timeout 30000
- name: Run codegen
run: npm run codegen
- name: Check for uncommitted changes
run: |
if ! git diff --exit-code src/gql/; then
echo "::error::Generated GraphQL types are out of date. Run 'npm run codegen' locally and commit."
exit 1
fi
See Also
- GraphQL Federation โ distributed GraphQL schemas
- GraphQL Subscriptions โ real-time with GraphQL
- GraphQL Persisted Queries โ performance optimization
- React Query Patterns โ server state management
Working With Viprasol
GraphQL Code Generator transforms GraphQL from a typed API into a fully type-safe data layer โ queries, mutations, fragment types, and resolver interfaces all derived from a single schema source. Our team sets up codegen pipelines, configures fragment masking for component isolation, and integrates schema drift detection into CI so type safety is never an afterthought.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need a Modern Web Application?
From landing pages to complex SaaS platforms โ we build it all with Next.js and React.
Free consultation โข No commitment โข Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse โฅ90 scores, mobile-first design, and full source code ownership. Senior engineers only โ from architecture through deployment.