Kurs
Ręcznie pisane typy TypeScript dla API GraphQL starzeją się w chwili, gdy ktoś zmieni schemat.
Załóżmy, że backendowiec zmienia nazwę pola title w typie Course na name. Frontend nadal się kompiluje, bo ręcznie napisana interfejs Course nie wie, że coś się zmieniło, więc błąd wychodzi dopiero w czasie działania. Prawdziwe aplikacje mają zwykle dziesiątki zapytań, mutacji i fragmentów, więc widzisz, jak szybko może to eskalować.
GraphQL Code Generator, w skrócie GraphQL Codegen, odczytuje twój schemat i operacje GraphQL, a następnie zamienia je w typy, helpery do zapytań, integracje z klientami i inny boilerplate, który w przeciwnym razie pisałbyś ręcznie. Najczęściej używa się go z TypeScriptem i popularnymi klientami, jak Apollo czy urql. Jedno polecenie przywraca zgodność typów za każdym razem, gdy schemat się zmieni.
W tym artykule pokażę ci, jak skonfigurować GraphQL Codegen w małym projekcie TypeScript, jak go ustawić, korzystać z generowanych typów i uruchamiać go automatycznie w twoim workflow.
Jak GraphQL wypada na tle REST? Przeczytaj nasz Graph QL vs REST: kompletny przewodnik, aby poznać różnice i zalety.
Czym jest GraphQL Codegen?
GraphQL Codegen to narzędzie wiersza poleceń, które zamienia definicje GraphQL w kod aplikacji.
To projekt open source od The Guild, instalowany jako zależność deweloperska. Łączy trzy elementy:
- Schemat GraphQL: Kontrakt między serwerem a klientami, z każdym typem, polem i argumentem wystawianym przez API
- Operacje GraphQL: Zapytania, mutacje, subskrypcje i fragmenty, które twoja aplikacja wysyła do API
- Generowany kod: Typy i helpery, które Codegen tworzy na podstawie schematu i twoich operacji
Schemat mówi Codegenowi, co jest możliwe, a operacje – o co prosi twoja aplikacja.
Codegen łączy oba elementy, dzięki czemu każdy wygenerowany typ odpowiada dokładnemu kształtowi odpowiedzi dla danej operacji. Sprawdza też każdą operację względem schematu. Zmienione z wprowadzenia pole title zatrzymałoby generowanie błędem w stylu Cannot query field "title" on type "Course", więc wychwycisz problem, zanim kod w ogóle się uruchomi.
Wynik zależy od wtyczek i konfiguracji. Sam Codegen nie pisze TypeScriptu. Robią to wtyczki. Na przykład wtyczka typescript generuje typy schematu, a typescript-operations generuje typy dla twoich zapytań i mutacji. Istnieją też wtyczki dla innych języków, jak Java czy C#, ale w tym artykule zostaniemy przy TypeScripcie.
Pamiętaj też, że Codegen nie zastępuje twojego serwera GraphQL.
Nie rozwiązuje zapytań ani nie pobiera danych. Podczas developmentu czyta definicje GraphQL i zapisuje pliki. Resztę robi twój serwer i klient GraphQL.
Jak działa GraphQL Codegen
Każda konfiguracja Codegen podąża tym samym schematem: schemat + operacje GraphQL -> konfiguracja Codegen -> wygenerowany kod.

Diagram przepływu pracy GraphQL Codegen
Ogólna idea wygląda tak:
-
Dostarcz schemat: Wskaż Codegenowi lokalny plik
.graphqllub działający endpoint GraphQL, który Codegen odczyta przez introspekcję -
Wskaż operacje: Użyj wzorców glob, np.
src/**/*.ts, aby Codegen znalazł zapytania, mutacje i fragmenty w twoim kodzie -
Skonfiguruj wtyczki i cele wyjściowe: Powiedz Codegenowi, które pliki ma zapisać i które wtyczki generują każdy z nich
-
Uruchom Codegen: Uruchom
npx graphql-codegen, aby wczytać schemat, zweryfikować operacje i zapisać wynik -
Użyj wygenerowanych artefaktów: Importuj wygenerowane typy i węzły dokumentów do kodu aplikacji
Kroki 1–3 trafiają do jednego pliku konfiguracyjnego, zwykle codegen.ts. Oto jego najmniejsza wersja dla projektu katalogu kursów:
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
// Step 1: Where the schema comes from
schema: "schema.graphql",
// Step 2: Where your operations are placed
documents: ["src/**/*.ts"],
// Step 3: What to generate and where to write it
generates: {
"./src/gql/": { preset: "client" },
},
};
export default config;
Preset client to pakiet wtyczek, który jednocześnie generuje typy schematu, typy operacji i typowane węzły dokumentów. Omówię każdy z tych elementów w sekcjach o konfiguracji i ustawieniach.
Jak skonfigurować GraphQL Codegen
Najlepiej zrozumiesz Codegen, uruchamiając go na prawdziwym projekcie.
W tej części zbudujesz od zera mały projekt katalogu kursów. Ma schemat z kursami i prowadzącymi, jedno zapytanie i konfigurację Codegen, która zmienia zapytanie w typy TypeScript. Wystarczy Node.js i npm.
Krok 1: Zainstaluj GraphQL Code Generator
Utwórz nowy folder i zainicjuj projekt npm:
mkdir course-catalog && cd course-catalog
npm init -y
Codegen potrzebuje kilku paczek. Dwie trafiają do zwykłych zależności, a reszta do devDependencies, bo działają tylko podczas developmentu:
npm install graphql @graphql-typed-document-node/core
npm install -D typescript @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node
Do czego służy każda paczka:
-
graphql: Referencyjna implementacja GraphQL dla JavaScript, której Codegen używa do parsowania schematu i operacji -
@graphql-typed-document-node/core: Dostarcza typTypedDocumentNode, który importuje wygenerowany kod -
@graphql-codegen/cli: Narzędzie wiersza poleceń Codegen -
@graphql-codegen/typescript-operations: Wtyczka generująca typy dla twoich zapytań, mutacji i fragmentów -
@graphql-codegen/typed-document-node: Wtyczka generująca obiekty dokumentów z dołączonymi typami
Krok 2: Utwórz plik konfiguracyjny
Codegen czyta ustawienia z pliku konfiguracyjnego w katalogu głównym projektu. Utwórz codegen.ts z pustą konfiguracją na razie:
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {};
export default config;
Możesz też uruchomić npx graphql-codegen init, aby odpowiedzieć na kilka pytań i dostać wygenerowany plik konfiguracyjny. Tutaj piszę go ręcznie, żeby pokazać, co robi każde ustawienie.
Krok 3: Wskaż lokalizacje schematu i dokumentów
Codegen potrzebuje dwóch wejść, więc utwórzmy oba.
Najpierw zapisz schemat jako schema.graphql w katalogu głównym projektu:
enum Level {
BEGINNER
INTERMEDIATE
ADVANCED
}
type Instructor {
id: ID!
name: String!
bio: String
}
type Course {
id: ID!
title: String!
level: Level!
durationHours: Int!
instructor: Instructor!
}
type Enrollment {
id: ID!
course: Course!
studentEmail: String!
}
type Query {
courses(level: Level, limit: Int): [Course!]!
course(id: ID!): Course
}
type Mutation {
enrollStudent(courseId: ID!, studentEmail: String!): Enrollment!
}
Następnie utwórz src/operations/courses.graphql z zapytaniem pobierającym kursy i ich prowadzących:
query GetCourses($level: Level, $limit: Int) {
courses(level: $level, limit: $limit) {
id
title
level
instructor {
name
}
}
}
Teraz wskaż Codegenowi oba pliki. Klucz schema przyjmuje ścieżkę do schematu, a documents – wzorzec glob pasujący do twoich operacji:
const config: CodegenConfig = {
schema: "schema.graphql",
documents: ["src/**/*.graphql"],
};
Krok 4: Wybierz plik wyjściowy
Klucz generates mówi Codegenowi, gdzie zapisać wynik. Każdy klucz wewnątrz to ścieżka do pliku:
const config: CodegenConfig = {
schema: "schema.graphql",
documents: ["src/**/*.graphql"],
generates: {
"src/generated/graphql.ts": {},
},
};
Codegen utworzy folder src/generated/, jeśli nie istnieje.
Krok 5: Dodaj odpowiednie wtyczki
Plik wyjściowy bez wtyczek będzie pusty. Dodaj dwie zainstalowane wtyczki:
import type { CodegenConfig } from "@graphql-codegen/cli";
const config: CodegenConfig = {
schema: "schema.graphql",
documents: ["src/**/*.graphql"],
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript-operations", "typed-document-node"],
},
},
};
export default config;
Jeśli korzystałeś ze starszego tutorialu, pewnie widziałeś też w tej liście wtyczkę typescript. Możesz ją teraz usunąć, bo typescript-operations działa samodzielnie i generuje tylko te typy Input, Enum i Operation, które faktycznie są używane. Jeśli w obecnych wersjach umieścisz obie w tym samym pliku wyjściowym, TypeScript zgłosi duplikaty deklaracji.
Krok 6: Uruchom Codegen
Dodaj skrypt npm, żeby nie musieć pamiętać komendy:
npm pkg set scripts.codegen="graphql-codegen"
Następnie uruchom:
npm run codegen
Codegen wczytuje schemat, weryfikuje zapytanie względem niego i zapisuje wynik:

Wyniki generowania Codegen
Jeśli operacja odwołuje się do pola nieistniejącego w schemacie, to na tym etapie uruchomienie się wysypie i żadne pliki nie zostaną zapisane.
Krok 7: Przejrzyj wygenerowany wynik
Otwórz src/generated/graphql.ts. Zobaczysz coś takiego:

Wygenerowany wynik
Codegen wygenerował cztery elementy:
-
Level: Unia stringów dla enuma, wygenerowana tylko dlatego, że zapytanie go używa -
GetCoursesQueryVariables: Typ dla zmiennych przekazywanych do zapytania, gdzie obie są opcjonalne, bo schemat nie oznacza ich jako wymaganych -
GetCoursesQuery: Kształt odpowiedzi, zawierający tylko pola żądane przez zapytanie -
GetCoursesDocument: Sparsowane zapytanie jako typowany węzeł dokumentu, z dołączonymi typami wyniku i zmiennych
Spójrz ponownie na GetCoursesQuery. Nie zawiera durationHours ani bio prowadzącego, bo zapytanie ich nie prosi. Jeśli spróbujesz odczytać w kodzie course.durationHours, dostaniesz błąd kompilacji, a nie undefined w runtime.
I to cały setup – ty piszesz GraphQL, a Codegen pisze TypeScript.
Jak konfigurować GraphQL Codegen
Większość konfiguracji Codegen, które zobaczysz, jest dłuższa niż ta powyżej, ale składają się z tych samych czterech ustawień.
Plik konfiguracyjny może mieć nazwę codegen.ts, codegen.yml lub codegen.json. Używam TypeScriptu, bo dostajesz podpowiedzi i sprawdzanie typów na samej konfiguracji. Ustawienia są takie same w każdym formacie.
Schema
Klucz schema mówi Codegenowi, gdzie znajduje się twój schemat. Akceptuje kilka rodzajów źródeł.
Lokalny plik lub wzorzec glob działa, gdy schemat jest w repozytorium:
schema: "schema.graphql",
// or, for a schema split across files
schema: "schema/**/*.graphql",
URL działa, gdy schemat jest na działającym serwerze. Codegen wysyła do endpointu zapytanie introspekcyjne i możesz przekazać nagłówki do autoryzacji:
schema: [
{
"https://api.example.com/graphql": {
headers: { Authorization: Bearer ${process.env.API_TOKEN} },
},
},
],
Opcja z URL działa tylko, jeśli serwer ma włączoną introspekcję. Wiele produkcyjnych API ją wyłącza, więc w CI bezpieczniej jest trzymać plik schematu w repozytorium.
Documents
Klucz documents mówi Codegenowi, gdzie znaleźć twoje zapytania, mutacje, subskrypcje i fragmenty.
Operacje mogą być w plikach .graphql albo inline w twoim kodzie TypeScript. Codegen znajduje operacje inline, gdy są owinięte w tag gql lub oznaczone komentarzem /* GraphQL */:
const EnrollStudent = /* GraphQL */
mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
id
course {
title
}
}
}
;
Aby skanować oba typy plików, poszerz wzorzec glob. Dodaj wzorzec negujący, żeby Codegen nie skanował własnego outputu:
documents: ["src/**/*.{graphql,ts}", "!src/generated/**"],
Nadaj każdej operacji unikalną nazwę. Codegen buduje nazwy typów z nazw operacji, więc EnrollStudent staje się EnrollStudentMutation i EnrollStudentMutationVariables.
Generates
Klucz generates mapuje ścieżki wyjściowe na to, co ma do nich trafić. Możesz mieć tyle celów wyjściowych, ile potrzebujesz, każdy z własnymi wtyczkami.
Załóżmy, że chcesz typy operacji w jednym pliku, a pełne typy schematu w innym:
generates: {
"src/generated/graphql.ts": {
plugins: ["typescript-operations", "typed-document-node"],
config: {
useTypeImports: true,
},
},
"src/generated/schema-types.ts": {
plugins: ["typescript"],
},
},
Obiekt config wewnątrz celu dotyczy tylko tego pliku. Tutaj useTypeImports sprawia, że wygenerowany plik używa import type, czego wymagają niektóre konfiguracje builda. Jeśli umieścisz config na korzeniu pliku, dotyczy każdego outputu.
Ścieżka wyjściowa może też być folderem kończącym się ukośnikiem, jak "src/gql/". Taki format stosują presety, bo zapisują więcej niż jeden plik.
Plugins
Wtyczki decydują, co zostanie wygenerowane. Codegen ładuje schemat i dokumenty, a każda wtyczka zamienia je w część wyjścia.
Oto wtyczki, które najczęściej zobaczysz w projektach TypeScript:
-
typescript-operations: Typy dla twoich zapytań, mutacji, subskrypcji i fragmentów, plus enumy i inputy, których używają -
typed-document-node: Obiekty dokumentów z dołączonymi typami wyniku i zmiennych, rozumiane przez większość klientów GraphQL -
typescript: Typy dla każdego obiektu, enuma i inputu w schemacie, niezależnie od tego, czy używają ich twoje operacje -
typescript-resolvers: Sygnatury typów dla funkcji resolverów po stronie serwera
Preset to pakiet wtyczek z sensowną domyślną konfiguracją. Preset client z sekcji Jak to działa jest zalecany przez The Guild dla aplikacji klienckich. Pod spodem bazuje na ulepszonym typescript-operations, więc dostajesz jego zalety bez dodatkowej konfiguracji.
Każda wtyczka ma własne opcje konfiguracyjne i jest ich sporo. Nie musisz uczyć się ich z góry. Zacznij od domyślnych i szukaj opcji, gdy wygenerowany wynik nie pasuje do oczekiwań twojego kodu.
Co może generować GraphQL Codegen?
To, co dostajesz z Codegen, zależy od wybranych wtyczek.
Ten sam schemat i operacje mogą wygenerować zwykłe typy TypeScript albo gotowy kod kliencki. Oto najczęściej używane wyjścia.
Typy TypeScript
Wtyczka typescript generuje typ dla każdego obiektu, enuma i inputu w twoim schemacie. Dla katalogu kursów typ Course wygląda tak:
export type Course = {
__typename?: 'Course';
durationHours: Scalars['Int']['output'];
id: Scalars['ID']['output'];
instructor: Instructor;
level: Level;
title: Scalars['String']['output'];
};
Te typy opisują pełny schemat, więc są przydatne po stronie serwera, w fixture’ach testowych i w helperach pracujących na kompletnych obiektach.
Ale słabo pasują do kodu klienckiego. Typ Course mówi, że każde pole istnieje, nawet te, o które twoje zapytanie w ogóle nie prosiło.
Typy operacji
Wtyczka typescript-operations generuje po dwa typy na operację: jeden dla wyniku i jeden dla zmiennych. Działa tak samo dla zapytań i subskrypcji.
Oto, co generuje dla mutacji EnrollStudent:
export type EnrollStudentMutationVariables = Exact<{
courseId: string | number;
studentEmail: string;
}>;
export type EnrollStudentMutation = { enrollStudent: { id: string, course: { title: string } } };
Typ wyniku odpowiada zbiorowi selekcji, pole po polu. Typ zmiennych to Exact, więc TypeScript odrzuci dodatkowe lub błędnie napisane zmienne.
Zmienna courseId akceptuje zarówno string, jak i number, bo scalar GraphQL ID przyjmuje oba jako wejście. Od v6 domyślny typ to string | number, co jest poprawne dla zastosowań klienckich.
Typowane węzły dokumentów
Zwykły DocumentNode to sparsowane zapytanie GraphQL. Zna strukturę zapytania, ale nie wie, co zapytanie zwraca ani jakie przyjmuje zmienne.
Typowany węzeł dokumentu ma oba te typy przy sobie. Wtyczka typed-document-node generuje jeden na operację:
export const GetCoursesDocument = { /* ... */ } as unknown as DocumentNode<GetCoursesQuery, GetCoursesQueryVariables>;
Dzięki temu dowolna funkcja, która przyjmuje TypedDocumentNode, może wywnioskować typy wyniku i zmiennych wyłącznie z dokumentu. Nie przekazujesz generyków ręcznie i nie połączysz zapytania z błędnymi typami.
Większość nowoczesnych klientów GraphQL akceptuje typowane węzły dokumentów, w tym Apollo Client, urql i graphql-request. Wszystkie trzy zobaczysz w akcji później w artykule.
Integracje specyficzne dla klientów
Niektóre wtyczki idą dalej i generują kod pod konkretnego klienta lub framework. Na przykład:
-
typescript-react-apollo: Generuje typowane hooki React dla Apollo Client -
typescript-urql: Generuje typowane hooki i komponenty dla urql -
typescript-react-query: Generuje typowane hooki dla TanStack Query -
typescript-graphql-request: Generuje typowane SDK z jedną funkcją na operację
Wszystkie te wtyczki są społecznościowe. The Guild przeniósł je do osobnego repozytorium community, więc tempo ich wydawania różni się od wtyczek rdzeniowych.
Potrzebujesz ich też rzadziej niż kiedyś. Gdy klient akceptuje typowane węzły dokumentów, generowane hooki nie dodają wiele bezpieczeństwa typów. Dokumentacja Apollo, na przykład, zaleca użycie wtyczki typescript-operations, która skupia się na generowaniu samych typów i nie dodaje dodatkowego kodu runtime.
Ogólnie zacznij od typów operacji i typowanych węzłów dokumentów. Potem dodaj wtyczkę specyficzną dla klienta tylko wtedy, gdy twój zespół chce generowanych hooków lub funkcji SDK.
GraphQL Codegen z TypeScriptem
Wygenerowane typy mają znaczenie tylko wtedy, gdy wyłapują błędy, więc zróbmy parę.
Ten przykład rozbudowuje projekt katalogu kursów o drugie zapytanie, mały helper wysyłający je do API i kod wykorzystujący wynik. Zakłada działający serwer GraphQL implementujący schemat pod adresem http://localhost:4000/graphql.
Oto część schematu używana w przykładzie:
type Instructor {
id: ID!
name: String!
bio: String
}
type Course {
id: ID!
title: String!
level: Level!
durationHours: Int!
instructor: Instructor!
}
type Query {
course(id: ID!): Course
}
Ważne są tu dwa detale. Pole course zwraca Course bez !, więc jest null, gdy żaden kurs nie pasuje do ID. bio prowadzącego też jest nullowalne.
Utwórz src/operations/course.graphql z zapytaniem pobierającym jeden kurs:
query GetCourse($id: ID!) {
course(id: $id) {
title
durationHours
instructor {
name
bio
}
}
}
Uruchom ponownie npm run codegen. Codegen doda dwa nowe typy do src/generated/graphql.ts:

Zawartość pliku graphql.ts
Nullowalność z schematu jest zachowana. course może być null, bio może być null, a id jest wymagane, bo zapytanie deklaruje je jako ID!.
Teraz kod aplikacji. Utwórz src/execute.ts z helperem wysyłającym dowolny typowany dokument do API:
import { print } from "graphql";
import type { TypedDocumentNode } from "@graphql-typed-document-node/core";
const API_URL = "http://localhost:4000/graphql";
export async function execute<TResult, TVariables>(
document: TypedDocumentNode<TResult, TVariables>,
variables: TVariables,
): Promise<TResult> {
const response = await fetch(API_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ query: print(document), variables }),
});
const { data, errors } = await response.json();
if (errors?.length) {
throw new Error(errors[0].message);
}
return data as TResult;
}
Helper nic nie wie o kursach. Oba typy dostaje z dokumentu, który mu przekażesz, a funkcja print() zmienia dokument z powrotem w string zapytania do żądania.
Następnie użyj go w src/course-details.ts:
import { execute } from "./execute";
import { GetCourseDocument, type GetCourseQuery } from "./generated/graphql";
function formatCourse(course: NonNullable<GetCourseQuery["course"]>): string {
const bio = course.instructor.bio ?? "No bio yet";
return course.title({course.durationHours}h) by ${course.instructor.name} - ${bio};
}
const { course } = await execute(GetCourseDocument, { id: "c1" });
if (course) {
console.log(formatCourse(course));
} else {
console.log("Course not found");
}
Funkcja formatCourse() bierze typ parametru z wygenerowanego typu zapytania. NonNullable usuwa przypadek null, bo funkcja działa dopiero po sprawdzeniu w if.
Przykład używa top-level await, więc ustaw "type": "module" w package.json i uruchom go przez npx tsx src/course-details.ts:

Udane wywołanie do mockowego serwera GraphQL
To zakłada, że masz uruchomiony serwer GraphQL. Oto mock, którego użyłem:
import { createServer } from "node:http";
import { readFileSync } from "node:fs";
import { createSchema, createYoga } from "graphql-yoga";
const instructors = [{ id: "i1", name: "Dario Radecic", bio: "Developer" }];
const courses = [
{ id: "c1", title: "Intro to GraphQL", level: "BEGINNER", durationHours: 4, instructor: instructors[0] },
{ id: "c2", title: "GraphQL Schema Design", level: "ADVANCED", durationHours: 6, instructor: instructors[0] },
];
const schema = createSchema({
typeDefs: readFileSync("schema.graphql", "utf8"),
resolvers: {
Query: {
courses: (_: unknown, args: { level?: string; limit?: number }) =>
courses
.filter((course) => !args.level || course.level === args.level)
.slice(0, args.limit ?? undefined),
course: (_: unknown, args: { id: string }) =>
courses.find((course) => course.id === args.id) ?? null,
},
Mutation: {
enrollStudent: (_: unknown, args: { courseId: string; studentEmail: string }) => ({
id: "e1",
course: courses.find((course) => course.id === args.courseId),
studentEmail: args.studentEmail,
}),
},
},
});
createServer(createYoga({ schema })).listen(4000, () => {
console.log("Mock API running at http://localhost:4000/graphql");
});
A teraz spróbuj to popsuć. Otwórz src/course-details.ts i zamień wszystko poniżej funkcji formatCourse() na trzy błędy, które ręcznie pisane typy często przepuszczają:
// Missing the required id variable
await execute(GetCourseDocument, {});
const { course } = await execute(GetCourseDocument, { id: "c1" });
// Reading a field the query doesn't request
console.log(course?.level);
// Forgetting that course can be null
console.log(course.title);
Następnie sprawdź typy projektu:
npx tsc -p .
TypeScript wyłapie wszystkie trzy, zanim kod się uruchomi:

Błędy wychwycone przez TypeScript
Tak Codegen zmniejsza niedopasowania między frontendem a API. Typy pochodzą z tego samego schematu, którego używa serwer, więc nie mogą opisywać pól, których nie ma, ani pomijać nullli, które serwer może zwrócić.
I ochrona nie kończy się po pierwszym uruchomieniu. Załóżmy, że zespół backendowy zrobi durationHours nullowalne. Regenerujesz, typ zmienia się na number | null, a TypeScript wskaże każdą linię, która zakładała liczbę.
GraphQL Codegen z klientami GraphQL
Codegen nie wysyła żądań. To zadanie dla twojego klienta GraphQL.
Wygenerowane typowane węzły dokumentów to punkt przekazania między nimi. Przekazujesz je do klienta, a klient wywnioskuje typy. Poniższe przykłady używają tego samego pliku src/generated/graphql.ts i mockowego API z poprzedniej sekcji, więc utrzymuj npx tsx mock/server.ts w osobnym terminalu.
GraphQL Codegen z Apollo
Zainstaluj Apollo Client i jego zależność peer rxjs:
npm install @apollo/client rxjs
Przykład uruchamia też mutację EnrollStudent. Zapisz ją jako src/operations/enroll.graphql:
mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
id
course {
title
}
}
}
Dokumentacja Apollo zaleca także dwie opcje konfiguracyjne dla Codegen, bo Apollo Client zawsze dodaje pola __typename, ale nie dodaje ich do typów root. Dodaj je do celu wyjściowego w codegen.ts:
"src/generated/graphql.ts": {
plugins: ["typescript-operations", "typed-document-node"],
config: {
nonOptionalTypename: true,
skipTypeNameForRoot: true,
},
},
Z tymi opcjami każdy obiekt w wygenerowanych typach wyników dostaje wymagane pole __typename, np. __typename: 'Course'. To odpowiada temu, co zwraca cache Apollo.
Uruchom ponownie npm run codegen, żeby wykryć nową mutację i konfigurację.
Apollo Client akceptuje typowane węzły dokumentów w query() i mutate(). Zapisz to jako src/apollo.ts:
import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client";
import { EnrollStudentDocument, GetCoursesDocument } from "./generated/graphql";
const client = new ApolloClient({
link: new HttpLink({ uri: "http://localhost:4000/graphql" }),
cache: new InMemoryCache(),
});
const { data } = await client.query({
query: GetCoursesDocument,
variables: { level: "BEGINNER", limit: 5 },
});
data?.courses.forEach((course) => console.log(course.title));
const result = await client.mutate({
mutation: EnrollStudentDocument,
variables: { courseId: "c1", studentEmail: "student@example.com" },
});
console.log(result.data?.enrollStudent.course.title);
Obiekt data ma typ GetCoursesQuery | undefined, a TypeScript sprawdza variables względem GetCoursesQueryVariables. Mutacja działa tak samo z EnrollStudentMutation i jej zmiennymi.
Uruchom przez npx tsx src/apollo.ts:

Wynik Apollo
Pierwsza linia pochodzi z zapytania, bo to jedyny kurs dla początkujących. Druga pochodzi z mutacji, która zwraca tytuł kursu, do którego zapisano studenta.
Hooki React, jak useQuery() i useMutation(), w ten sam sposób wnioskują typy z typowanych węzłów dokumentów.
GraphQL Codegen z urql
Zainstaluj paczkę core urql:
npm install @urql/core
urql działa z typowanymi węzłami dokumentów od ręki. Przekaż dokument jako pierwszy argument do query() w src/urql.ts:
import { Client, cacheExchange, fetchExchange } from "@urql/core";
import { GetCoursesDocument } from "./generated/graphql";
const client = new Client({
url: "http://localhost:4000/graphql",
exchanges: [cacheExchange, fetchExchange],
});
const result = await client
.query(GetCoursesDocument, { level: "ADVANCED" })
.toPromise();
if (result.error) {
throw result.error;
}
result.data?.courses.forEach((course) => console.log(course.title));
Drugi argument ma typ zmiennych zapytania, a result.data – typ wyniku zapytania.
Uruchom przez npx tsx src/urql.ts:

Wynik urql
Powiązania frameworków, jak useQuery() w paczce urql dla Reacta, też akceptują typowane węzły dokumentów.
GraphQL Codegen z innymi klientami
Nie potrzebujesz pełnoprawnego klienta, żeby dostać typowane wyniki.
Klient graphql-request to minimalny klient wysyłający jedno żądanie i zwracający dane. Zainstaluj go poleceniem:
npm install graphql-request
Wnioskuje typy z typowanych węzłów dokumentów tak samo jak Apollo i urql. Zapisz to jako src/graphql-request.ts:
import { request } from "graphql-request";
import { GetCourseDocument } from "./generated/graphql";
const { course } = await request(
"http://localhost:4000/graphql",
GetCourseDocument,
{ id: "c2" },
);
console.log(course?.title);
Uruchom przez npx tsx src/graphql-request.ts:

Wynik graphql-request
Ten sam pomysł działa z TanStack Query. Sam nie wysyła zapytań GraphQL, więc wywołujesz graphql-request albo helper execute() z poprzedniej sekcji wewnątrz funkcji zapytania, a typ wyniku przepływa do twojego kodu.
A jeśli twój klient w ogóle nie wspiera typowanych węzłów dokumentów, nadal możesz używać zwykłych typów operacji. Zaimportuj GetCourseQuery i GetCourseQueryVariables i przekaż je jako generyki. To więcej ręcznej pracy, ale typy nadal pochodzą ze schematu.
Jak uruchamiać GraphQL Codegen automatycznie
Wygenerowane typy pomagają tylko wtedy, gdy odpowiadają aktualnemu schematowi i operacjom.
Jeśli zapomnisz ponownie uruchomić Codegen po zmianie, typy opisują kod, którego już nie ma. To ten sam problem dryfu, co przy typach pisanych ręcznie, tylko z dodatkowymi krokami. Rozwiązanie to wyjęcie ręcznego kroku z workflow.
Skrypty npm
Zacznij od kilku skryptów w package.json, żeby każdy w zespole uruchamiał Codegen w ten sam sposób:
{
"scripts": {
"codegen": "graphql-codegen",
"codegen:watch": "graphql-codegen --watch",
"codegen:check": "graphql-codegen --check",
"prebuild": "npm run codegen",
"build": "tsc -p ."
}
}
Dalej w tej sekcji znajdziesz, do czego służy każdy z nich.
Tryb watch
Tryb watch ponownie uruchamia Codegen za każdym razem, gdy zmieni się obserwowany plik. Potrzebuje paczki @parcel/watcher, której Codegen nie instaluje za ciebie:
npm install -D @parcel/watcher
npm run codegen:watch
Jeśli pominiesz instalację, Codegen uruchomi się raz i wypisze Failed to import @parcel/watcher zamiast obserwować.
Tryb watch śledzi twoje operacje i lokalny plik schematu. Jeśli dodasz pole do course.graphql, nowe pole pojawi się w GetCourseQuery sekundę później. Jeśli zrobisz durationHours nullowalne w schema.graphql, typ zmieni się na number | null, a edytor oflaguje linie, które zakładały liczbę.
Trzymaj go uruchomionego w terminalu obok serwera deweloperskiego podczas pracy.
Generuj przed buildami
npm uruchamia skrypt o nazwie prebuild przed build. Z powyższymi skryptami npm run build najpierw regeneruje typy, a dopiero potem uruchamia kompilator TypeScript.
Dzięki temu build nigdy nie działa na nieaktualnych typach, nawet jeśli ktoś zapomniał uruchomić Codegen po pobraniu zmian.
CI/CD
Flaga --check uruchamia Codegen w trybie dry-run. Nie zapisuje plików, porównuje to, co wygenerowałby, z tym, co jest na dysku, i wychodzi z kodem 1, jeśli cokolwiek się różni.
Oto minimalny workflow GitHub Actions, który z niej korzysta:
name: CI
on: [push, pull_request]
jobs:
typecheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm run codegen:check
- run: npm run build
Krok codegen:check ma sens tylko wtedy, gdy commitujesz wygenerowane pliki do repo. Jeśli nie, pomiń go, bo npm run build i tak wszystko zregeneruje przez hook prebuild.
Regeneruj przy zmianach schematu
Operacje zmieniają się w twoim repo, więc watch i prebuild je pokrywają. Zmiany schematu są trudniejsze, bo zwykle pochodzą od innego zespołu.
Jak je obsłużysz, zależy od miejsca schematu:
- Plik schematu w tym samym repo: Tryb watch i CI wykryją zmiany – to najłatwiejsza konfiguracja pracy
- Schemat z URL: Codegen pobiera najnowszą wersję przy każdym uruchomieniu, więc
codegen:checkw CI padnie, gdy tylko schemat serwera przestanie pasować do twojego zacommitowanego outputu - Schemat publikowany przez inne repo: Skopiuj lub pobierz go w trakcie builda, a potem uruchom Codegen na lokalnej kopii
Opcja z URL ma jeden haczyk. Tryb watch reaguje na zmiany plików, więc nie zauważa, gdy zmienia się zdalny schemat. Zobaczysz te zmiany dopiero przy kolejnym ręcznym uruchomieniu lub jobie CI.
GraphQL Codegen vs typy pisane ręcznie
Oba podejścia dają ci typy TypeScript dla danych GraphQL, ale są między nimi różnice, o których warto wiedzieć.
Typy pisane ręcznie
Przy ręcznych typach czytasz schemat i samemu piszesz odpowiadające mu interfejsy.
Masz pełną kontrolę. Możesz nazywać typy, jak chcesz, łączyć kilka operacji w jeden współdzielony typ albo dodać pomocnicze typy, których nie ma w schemacie.
Ale to ręczna robota. Każde nowe pole czy zmieniona nazwa argumentu oznacza, że musisz znaleźć i zaktualizować pasujący typ ręcznie. I nic cię nie ostrzeże, gdy coś pominiesz.
Stąd bierze się dryf. Interfejs Course z wprowadzenia nadal się kompilował po tym, jak backend zmienił title, bo TypeScript sprawdza twój kod względem twoich typów, a nie względem API.
Typy generowane
Typy generowane pochodzą z tego samego schematu, którego używa serwer, więc z założenia są zsynchronizowane z twoimi definicjami GraphQL. Zmiana schematu plus uruchomienie Codegen aktualizuje wszystkie dotknięte typy naraz.
Piszesz też znacznie mniej kodu. Projekt katalogu kursów ma trzy operacje i zero ręcznie pisanych typów dla którejkolwiek z nich.
I niemal za darmo dostajesz development oparty o schemat. Zmieniasz schemat lub operację, regenerujesz, a kompilator daje ci listę wszystkiego, co trzeba poprawić.
Ale pamiętaj, że Codegen odzwierciedla tylko to, co mu dasz.
Jeśli schemat oznacza każde pole jako nullowalne, twoje wygenerowane typy będą pełne sprawdzeń | null. Jeśli operacja pobiera 40 pól, gdy strona potrzebuje pięciu, wygenerowany typ ma 40 pól.
Szybkie podsumowanie różnic:
| Typy pisane ręcznie | Typy generowane | |
|---|---|---|
| Utrzymanie | Ręczne przy każdej zmianie schematu | Jedno polecenie po każdej zmianie |
| Dryf schematu | Możliwy i cichy | Wychwycony przy generowaniu lub kompilacji |
| Boilerplate | Rośnie z każdą operacją | Nic do pisania ręcznie |
| Kontrola nad typami custom | Pełna | Ograniczona do opcji wtyczek |
| Development oparty o schemat | Ręczne krzyżowe sprawdzanie | Kompilator pokazuje każdą dotkniętą linię |
Codegen w porównaniu z typami pisanymi ręcznie
GraphQL Codegen vs. introspekcja GraphQL
Te dwa pojęcia są mylone, bo Codegen często używa introspekcji. Nadal rozwiązują różne problemy.
Introspekcja to funkcja specyfikacji GraphQL. Każdy serwer GraphQL może odpowiadać na zapytania o własny schemat przez specjalne pola, jak __schema i __type. Wynik to opis JSON każdego typu, pola i argumentu wystawianego przez API.
Po ludzku: introspekcja odpowiada na pytanie „Jak wygląda to API?”
Codegen odpowiada na „Jakiego kodu potrzebuje moja aplikacja?”. Bierze informacje o schemacie i twoje operacje, a potem zapisuje typy TypeScript i typowane węzły dokumentów.
Gdy ustawisz schema na URL w codegen.ts, Codegen wysyła do serwera zapytanie introspekcyjne, by pobrać schemat. Z lokalnym plikiem .graphql introspekcja w ogóle nie zachodzi.
Codegen może też wyprodukować wynik introspekcji jako output. Wtyczka @graphql-codegen/introspection zapisuje schemat jako JSON z introspekcją, którego używają niektóre narzędzia, np. Graphcache urql, do cache’owania z wiedzą o schemacie.
| Introspekcja | GraphQL Codegen | |
|---|---|---|
| Czym jest | Wbudowany system zapytań GraphQL | Narzędzie CLI i wtyczki |
| Wejście | Zapytanie do działającego serwera | Schemat i twoje operacje |
| Wyjście | JSON z opisem schematu | Typy, węzły dokumentów, kod klienta |
| Kiedy działa | Ilekroć klient pyta serwer | Podczas developmentu, buildów i w CI |
Codegen w porównaniu z introspekcją
Typowe problemy z GraphQL Codegen
Większość błędów Codegen sprowadza się do jednego z czterech problemów.
Brak wygenerowanych typów
Codegen działa bez błędów, ale oczekiwanego typu nie ma w pliku wyjściowym.
Sprawdź po kolei trzy rzeczy:
-
Schemat: Jeśli typu nie ma w schemacie, który ładuje Codegen, nie da się go wygenerować, więc otwórz plik schematu albo sam wyślij zapytanie introspekcyjne i potwierdź, że tam jest
-
Dokumenty: Typy operacji istnieją tylko dla operacji, które Codegen znalazł, więc jeśli plik z twoim zapytaniem nie pasuje do globu
documents, jego typów zabraknie -
Wtyczki: Od v6
typescript-operationsgeneruje tylko enumy i inputy używane przez twoje operacje, więc jeśli potrzebujesz pełnego typu schematu, jakCourse, dodaj wtyczkętypescriptdo osobnego pliku wyjściowego
Jeśli użyjesz wtyczki, której nie zainstalowałeś, Codegen zakończy się czytelnym komunikatem:
Unable to find template plugin matching 'typescript-react-apollo'
Install one of the following packages:
- @graphql-codegen/typescript-react-apollo
Codegen nie znajduje operacji
Ten błąd oznacza, że glob documents nie dopasował żadnego pliku z operacją:
Unable to find any GraphQL type definitions for the following pointers:
- src/**/*.gql
Sprawdź, czy rozszerzenie w globie pasuje do twoich plików. Wzorzec .gql nie znajdzie plików .graphql, a wzorzec src/**/*.graphql nie znajdzie operacji inline w plikach .ts.
Dla operacji inline sprawdź też tag. Codegen znajduje tylko literały szablonowe owinięte w gql albo oznaczone komentarzem /* GraphQL */. Zwykły template string z zapytaniem w środku jest dla niego niewidoczny.
Jeśli ustawisz ignoreNoDocuments: true, Codegen nie pokaże tego błędu i wygeneruje output bez typów operacji. Ta opcja jest przydatna w zupełnie nowym projekcie, ale później może ukryć zepsuty glob.
Wygenerowane typy są nieaktualne
Typy się kompilują, ale nie odzwierciedlają ostatniej zmiany w operacji lub schemacie.
Codegen sam się nie uruchamia. Odpal go po każdej zmianie pliku .graphql, operacji inline, schematu lub codegen.ts. Podczas developmentu robi to za ciebie npm run codegen:watch.
Jeśli typy są nieaktualne w CI, zacommituj zregenerowane pliki albo pozwól hookowi prebuild zregenerować je przed buildem.
Wygenerowane typy nie pasują do API
Typy się kompilują, ale dane z serwera mają inny kształt w runtime.
Prawie zawsze oznacza to, że Codegen i twoja aplikacja rozmawiają z różnymi schematami. Na przykład Codegen czyta lokalny schema.graphql sprzed tygodnia, a aplikacja woła produkcyjne API, które od tego czasu się zmieniło.
Sprawdź, gdzie wskazuje klucz schema. Jeśli to plik, porównaj go ze schematem API, które woła twoja aplikacja, i zaktualizuj. Jeśli to URL, upewnij się, że to to samo środowisko, którego używa aplikacja, a nie staging z nowszym lub starszym schematem.
Następnie zregeneruj i przejrzyj diff. Jeśli zmienił się plik operacji, a plik wygenerowany nie, operacja prawdopodobnie jest poza globem documents.
Najlepsze praktyki dla GraphQL Codegen
Codegen nie potrzebuje dużo konfiguracji, żeby działać dobrze. Te praktyki pomagają utrzymać ten stan wraz ze wzrostem projektu:
-
Trzymaj schemat i wygenerowane pliki w zgodzie: Uruchamiaj tryb watch podczas developmentu i
prebuildprzed buildami, żeby nikt nie pracował na typach ze starszego schematu -
Generuj typy specyficzne dla operacji: Używaj
typescript-operationsw kodzie klienckim, bo dokładnie odpowiadają temu, co pobiera każde zapytanie -
Commituj wygenerowane pliki tylko, gdy wasz workflow tego potrzebuje: Zacommitowane pliki pozwalają przeglądać zmiany typów w pull requestach i uruchamiać
--checkw CI, a jeśli zespół tego nie potrzebuje, dodaj folder output do.gitignorei generuj przy każdym buildzie -
Trzymaj konfigurację krótką: Jeden plik wyjściowy z dwiema wtyczkami wystarczy w większości projektów klienckich, a każda dodatkowa opcja to coś, co następny deweloper musi zrozumieć przed zmianami
-
Uruchamiaj Codegen w CI przy każdym pushu: Krok
--checkalbo hookprebuildwyłapuje zmiany, których ktoś nie zregenerował lokalnie -
Nigdy nie edytuj ręcznie wygenerowanych plików: Następne uruchomienie Codegen nadpisze twoje zmiany, więc popraw schemat, operację albo konfigurację
Podsumowanie
GraphQL Codegen przejmuje najbardziej powtarzalną część pracy z GraphQL – ręczne zamienianie schematów i operacji na typy TypeScript.
Workflow pozostaje taki sam niezależnie od wielkości projektu. Definiujesz schemat i operacje, konfigurujesz Codegen, generujesz typy i typowane węzły dokumentów i importujesz je do kodu aplikacji.
Największa wartość polega na tym, że twoje typy zawsze opisują API, które naprawdę wywołujesz, a nie kod, którego nie musisz pisać. Gdy schemat się zmieni, kompilator powie ci, jeśli coś się zepsuło, więc złapiesz błędy wcześnie.
Jeśli chcesz zobaczyć, jak GraphQL jest używany we współczesnej bazie NoSQL, przeczytaj nasz wpis MongoDB i GraphQL.
GraphQL Codegen – FAQ
Do czego służy GraphQL Codegen?
GraphQL Codegen generuje kod ze schematu GraphQL i twoich operacji GraphQL. Większość zespołów używa go do uzyskania typów TypeScript i typowanych węzłów dokumentów dla zapytań i mutacji, żeby nie pisać i nie aktualizować tych typów ręcznie. Może też generować typy resolverów dla serwerów i gotowe hooki dla konkretnych klientów.
Czy potrzebuję działającego serwera GraphQL, żeby używać Codegen?
Nie. Codegen może czytać schemat z lokalnego pliku .graphql, więc działa bez żadnego serwera. Potrzebujesz działającego serwera tylko wtedy, gdy klucz schema wskazuje na URL, bo wtedy Codegen pobiera schemat przez introspekcję.
Czy powinienem commitować wygenerowane pliki do Gita?
To zależy od workflow twojego zespołu. Zacommitowane pliki pozwalają przeglądać zmiany typów w pull requestach i używać graphql-codegen --check w CI, by wykrywać nieaktualny output. Jeśli tego nie potrzebujesz, dodaj folder output do .gitignore i regeneruj pliki przed każdym buildem.
Czy używać presetu client czy pojedynczych wtyczek?
The Guild zaleca preset client dla większości aplikacji klienckich, bo w jednym setupie łączy typy operacji, typowane węzły dokumentów i maskowanie fragmentów. Apollo zaleca zamiast tego wtyczkę typescript-operations, bo preset dodaje kod runtime i funkcje kolidujące z Apollo Client. Jeśli używasz Apollo, wybierz wtyczki. W przeciwnym razie preset to dobry domyślny wybór.
Dlaczego po aktualizacji Codegen dostaję błędy duplikatów deklaracji?
Od v6 wtyczka typescript-operations generuje własne typy enumów i inputów. Jeśli twoja konfiguracja nadal wymienia zarówno typescript, jak i typescript-operations dla tego samego pliku wyjściowego, obie wtyczki deklarują typy takie jak Level, a TypeScript zgłasza błędy duplikatów. Usuń wtyczkę typescript z tego outputu albo przenieś ją do osobnego pliku, jeśli nadal potrzebujesz pełnych typów schematu.