Back to Blog

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.

Viprasol Tech Team
October 9, 2026
13 min read

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


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.

GraphQL engineering โ†’ | Start a project โ†’

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

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

Viprasol ยท Web Development

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.