Chuyển đến nội dung chính

GraphQL Codegen: Cách tạo mã an toàn kiểu từ một Schema GraphQL

Hướng dẫn thực hành về GraphQL Code Generator bao gồm thiết lập và cấu hình, các kiểu TypeScript và typed document nodes được tạo, tích hợp với Apollo, urql và graphql-request, và tự động hóa với chế độ watch và CI.
Đã cập nhật 2 thg 10, 2026  · 12 phút đọc

Khám phá cùng AI

ChatGPTClaudePerplexity

Các kiểu TypeScript viết tay cho một API GraphQL sẽ lỗi thời ngay khi có ai đó thay đổi schema.

Giả sử một lập trình viên backend đổi tên trường title trên kiểu Course thành name. Frontend vẫn biên dịch vì interface Course viết tay không biết có gì thay đổi, nên lỗi chỉ xuất hiện lúc chạy. Ứng dụng thực tế thường có hàng chục queries, mutations và fragments, nên bạn sẽ thấy vấn đề có thể leo thang nhanh thế nào.

GraphQL Code Generator, gọi tắt là GraphQL Codegen, đọc schema và các operation GraphQL của bạn và chuyển chúng thành types, query helper, tích hợp client, và các phần mã khuôn mẫu khác mà nếu không bạn phải viết tay. Công cụ này thường dùng với TypeScript và các client phổ biến như Apollo và urql. Chỉ với một lệnh, types của bạn sẽ được đồng bộ lại bất cứ khi nào schema thay đổi.

Trong bài viết này, tôi sẽ hướng dẫn bạn cách thiết lập GraphQL Codegen trong một dự án TypeScript nhỏ, cấu hình nó, sử dụng các types được tạo ra, và chạy tự động như một phần trong quy trình làm việc.

GraphQL so với REST như thế nào? Hãy đọc Graph QL vs REST: Hướng dẫn đầy đủ của chúng tôi để tìm hiểu sự khác biệt và ưu điểm.

GraphQL Codegen là gì?

GraphQL Codegen là một công cụ dòng lệnh chuyển các định nghĩa GraphQL thành mã ứng dụng.

Đây là dự án mã nguồn mở của The Guild, bạn cài đặt như một dev dependency trong dự án. Nó kết nối ba thứ:

  • Schema GraphQL: Hợp đồng giữa server và client, với mọi kiểu, trường và tham số mà API cung cấp
  • Operations GraphQL: Các query, mutation, subscription và fragment mà ứng dụng của bạn gửi tới API
  • Mã được tạo ra: Các kiểu và helper mà Codegen viết dựa trên schema và operations của bạn

Schema cho Codegen biết điều gì là khả thi và các operation cho biết ứng dụng của bạn yêu cầu những gì.

Codegen kết hợp cả hai, nên mỗi kiểu được tạo ra khớp chính xác với cấu trúc phản hồi cho một operation. Nó cũng kiểm tra mọi operation so với schema. Trường title đã đổi tên trong phần mở đầu sẽ khiến quá trình tạo dừng với lỗi như Cannot query field "title" on type "Course", và bạn sẽ bắt được lỗi trước khi mã chạy.

Kết quả đầu ra phụ thuộc vào plugin và cấu hình của bạn. Bản thân Codegen không viết TypeScript. Plugin làm điều đó. Ví dụ, plugin typescript tạo types cho schema, và plugin typescript-operations tạo types cho queries và mutations của bạn. Cũng có plugin cho các ngôn ngữ khác như Java và C#, nhưng bài viết này chỉ tập trung vào TypeScript.

Điểm cần nhớ là Codegen không thay thế server GraphQL của bạn.

Nó không resolve query hay fetch dữ liệu. Nó đọc các định nghĩa GraphQL trong quá trình phát triển và ghi file. Server và GraphQL client của bạn sẽ làm phần còn lại.

GraphQL Codegen hoạt động như thế nào

Mọi thiết lập Codegen đều theo cùng luồng: schema + operations GraphQL -> cấu hình Codegen -> mã được tạo.

Sơ đồ quy trình làm việc của GraphQL Codegen

Sơ đồ quy trình làm việc của GraphQL Codegen

Ý tưởng chung là thế này:

  1. Cung cấp schema: Trỏ Codegen tới file .graphql cục bộ hoặc endpoint GraphQL đang chạy, Codegen sẽ đọc qua introspection

  2. Trỏ Codegen tới operations của bạn: Dùng mẫu glob như src/**/*.ts để Codegen tìm queries, mutations và fragments trong mã của bạn

  3. Cấu hình plugin và đích xuất: Chỉ định Codegen ghi file nào và plugin nào sẽ tạo mỗi file

  4. Chạy Codegen: Chạy npx graphql-codegen để tải schema, xác thực operations của bạn và ghi đầu ra

  5. Sử dụng artifacts được tạo: Import các types và document node được tạo vào mã ứng dụng của bạn

Các bước 1 đến 3 được đặt trong một file cấu hình, thường là codegen.ts. Đây là phiên bản nhỏ nhất cho một dự án danh mục khóa học:

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 là một gói plugin tạo types cho schema, types cho operations, và typed document nodes trong một lần. Tôi sẽ nói chi tiết từng phần trong các mục thiết lập và cấu hình.

Cách thiết lập GraphQL Codegen

Cách tốt nhất để hiểu Codegen là chạy nó trên một dự án thực.

Trong phần này, bạn sẽ xây dựng từ đầu một dự án danh mục khóa học nhỏ. Nó có schema với khóa học và giảng viên, một query, và một cấu hình Codegen chuyển query thành các kiểu TypeScript. Bạn chỉ cần Node.js và npm.

Bước 1: Cài đặt GraphQL Code Generator

Tạo thư mục mới và khởi tạo một dự án npm:

mkdir course-catalog && cd course-catalog
npm init -y

Codegen cần một vài gói. Hai gói vào dependencies thông thường, phần còn lại là dev dependencies vì chỉ chạy trong quá trình phát triển:

npm install graphql @graphql-typed-document-node/core
npm install -D typescript @graphql-codegen/cli @graphql-codegen/typescript-operations @graphql-codegen/typed-document-node

Vai trò của từng gói như sau:

  • graphql: Triển khai GraphQL tham chiếu cho JavaScript, Codegen dùng để parse schema và operations

  • @graphql-typed-document-node/core: Cung cấp kiểu TypedDocumentNode mà mã được tạo ra sẽ import

  • @graphql-codegen/cli: Công cụ dòng lệnh của Codegen

  • @graphql-codegen/typescript-operations: Plugin tạo types cho queries, mutations và fragments của bạn

  • @graphql-codegen/typed-document-node: Plugin tạo các đối tượng document kèm theo types

Bước 2: Tạo file cấu hình

Codegen đọc thiết lập từ một file cấu hình ở thư mục gốc dự án. Tạo codegen.ts với cấu hình trống trước:

import type { CodegenConfig } from "@graphql-codegen/cli";

const config: CodegenConfig = {};

export default config;

Bạn cũng có thể chạy npx graphql-codegen init để trả lời vài câu hỏi và nhận một file cấu hình tạo sẵn. Tôi viết tay ở đây để bạn thấy từng thiết lập làm gì.

Bước 3: Chỉ định vị trí schema và documents

Codegen cần hai đầu vào, nên hãy tạo cả hai.

Đầu tiên, lưu schema thành schema.graphql ở thư mục gốc dự án:

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!
}

Sau đó, tạo src/operations/courses.graphql với một query lấy các khóa học và giảng viên của chúng:

query GetCourses($level: Level, $limit: Int) {
  courses(level: $level, limit: $limit) {
    id
    title
    level
    instructor {
      name
    }
  }
}

Bây giờ hãy trỏ Codegen tới cả hai file. Khóa schema nhận đường dẫn schema, và documents nhận một mẫu glob khớp với các operation của bạn:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
};

Bước 4: Chọn file đầu ra

Khóa generates cho Codegen biết nơi ghi đầu ra. Mỗi khóa bên trong là một đường dẫn file:

const config: CodegenConfig = {
  schema: "schema.graphql",
  documents: ["src/**/*.graphql"],
  generates: {
    "src/generated/graphql.ts": {},
  },
};

Codegen sẽ tạo thư mục src/generated/ nếu chưa tồn tại.

Bước 5: Thêm các plugin liên quan

Một file đầu ra không có plugin sẽ trống. Thêm hai plugin bạn đã cài:

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;

Nếu bạn theo một hướng dẫn cũ, có lẽ bạn từng thấy plugin typescript trong danh sách này. Bạn có thể bỏ nó đi, vì typescript-operations tự hoạt động và chỉ tạo các kiểu Input, Enum và Operation thực sự được sử dụng. Nếu để cả hai trong cùng một file đầu ra ở các phiên bản hiện tại, TypeScript sẽ ném lỗi khai báo trùng lặp.

Bước 6: Chạy Codegen

Thêm một script npm để bạn không phải nhớ lệnh:

npm pkg set scripts.codegen="graphql-codegen"

Sau đó chạy nó:

npm run codegen

Codegen tải schema, xác thực query so với schema và ghi đầu ra:

Kết quả tạo file của Codegen

Kết quả tạo file của Codegen

Nếu một operation tham chiếu tới trường không tồn tại trong schema, quá trình chạy sẽ thất bại tại đây và không file nào được ghi.

Bước 7: Kiểm tra đầu ra được tạo

Mở src/generated/graphql.ts. Bạn sẽ thấy:

Đầu ra được tạo

Codegen đã tạo bốn thứ:

  • Level: Union chuỗi cho enum, chỉ được tạo vì query sử dụng nó

  • GetCoursesQueryVariables: Kiểu cho biến bạn truyền vào query, cả hai đều tùy chọn vì schema không đánh dấu bắt buộc

  • GetCoursesQuery: Hình dạng của phản hồi, chỉ với các trường mà query yêu cầu

  • GetCoursesDocument: Query đã parse dưới dạng typed document node, kèm theo kiểu kết quả và biến

Xem lại GetCoursesQuery. Nó không gồm durationHours hay bio của giảng viên, vì query không yêu cầu. Nếu bạn cố đọc course.durationHours trong mã, bạn sẽ nhận lỗi biên dịch, không phải undefined khi chạy.

Và đó là toàn bộ thiết lập — bạn viết GraphQL, và Codegen viết TypeScript.

Cách cấu hình GraphQL Codegen

Hầu hết các cấu hình Codegen bạn thấy sẽ dài hơn ví dụ trên, nhưng đều xây từ bốn thiết lập giống nhau.

File cấu hình có thể là codegen.ts, codegen.yml, hoặc codegen.json. Tôi dùng TypeScript vì bạn có autocomplete và kiểm tra kiểu ngay trên file cấu hình. Các thiết lập giống nhau ở mọi định dạng.

Schema

Khóa schema cho Codegen biết schema của bạn nằm ở đâu. Nó chấp nhận vài loại nguồn.

File cục bộ hoặc mẫu glob dùng khi schema nằm trong repo của bạn:

schema: "schema.graphql",
// or, for a schema split across files
schema: "schema/**/*.graphql",

URL dùng khi schema nằm trên server đang chạy. Codegen gửi một introspection query tới endpoint, và bạn có thể truyền headers để xác thực:

schema: [
  {
    "https://api.example.com/graphql": {
      headers: { Authorization: Bearer ${process.env.API_TOKEN} },
    },
  },
],

Tùy chọn URL chỉ hoạt động nếu server bật introspection. Nhiều API production tắt nó, nên cho CI, một file schema commit trong repo thường an toàn hơn.

Documents

Khóa documents cho Codegen biết nơi tìm queries, mutations, subscriptions và fragments của bạn.

Operations có thể nằm trong file .graphql hoặc inline trong mã TypeScript. Codegen tìm thấy inline operations khi chúng được bọc trong thẻ gql hoặc đánh dấu bằng comment /* GraphQL */:

const EnrollStudent = /* GraphQL */ 
  mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
    enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
      id
      course {
        title
      }
    }
  }
;

Để quét cả hai loại file, hãy mở rộng glob. Thêm mẫu phủ định để Codegen không quét chính đầu ra của nó:

documents: ["src/**/*.{graphql,ts}", "!src/generated/**"],

Đặt tên duy nhất cho mỗi operation. Codegen xây tên kiểu từ tên operation, nên EnrollStudent thành EnrollStudentMutation và EnrollStudentMutationVariables.

Generates

Khóa generates ánh xạ đường dẫn đầu ra tới nội dung trong đó. Bạn có thể có bao nhiêu đích đầu ra tùy ý, mỗi đích với plugin riêng.

Giả sử bạn muốn types cho operations ở một file và types đầy đủ cho schema ở file khác:

generates: {
  "src/generated/graphql.ts": {
    plugins: ["typescript-operations", "typed-document-node"],
    config: {
      useTypeImports: true,
    },
  },
  "src/generated/schema-types.ts": {
    plugins: ["typescript"],
  },
},

Đối tượng config bên trong một đích đầu ra chỉ áp dụng cho file đó. Ở đây, useTypeImports khiến file sinh ra dùng import type, điều mà một số thiết lập build yêu cầu. Nếu đặt config ở gốc file, nó áp dụng cho mọi đầu ra.

Một đường dẫn đầu ra cũng có thể là thư mục kết thúc bằng dấu gạch chéo, như "src/gql/". Đó là định dạng mà preset dùng, vì chúng ghi nhiều hơn một file.

Plugins

Plugin quyết định những gì được tạo. Codegen tải schema và documents, và mỗi plugin chuyển chúng thành một phần đầu ra.

Đây là các plugin bạn sẽ thường thấy trong dự án TypeScript:

  • typescript-operations: Các kiểu cho queries, mutations, subscriptions và fragments của bạn, cùng các enum và input mà chúng sử dụng

  • typed-document-node: Các đối tượng document kèm theo kiểu kết quả và biến, hầu hết GraphQL client đều hiểu

  • typescript: Các kiểu cho mọi object, enum và input trong schema, bất kể operations của bạn có dùng hay không

  • typescript-resolvers: Chữ ký kiểu cho các hàm resolver phía server

Một preset là một gói plugin với cấu hình mặc định hợp lý. Preset client từ phần Cách hoạt động là preset The Guild khuyến nghị cho ứng dụng client. Nó dựa trên typescript-operations đã được cải tiến bên dưới, nên bạn nhận lợi ích mà không cần thiết lập thêm.

Mỗi plugin có các tùy chọn cấu hình riêng, và có khá nhiều. Bạn không cần học trước. Hãy bắt đầu với mặc định và tra cứu tùy chọn khi đầu ra không khớp với kỳ vọng của mã bạn.

GraphQL Codegen có thể tạo ra gì?

Những gì bạn nhận được từ Codegen phụ thuộc vào plugin bạn chọn.

Cùng một schema và operations có thể tạo ra các kiểu TypeScript thuần túy hoặc mã client sẵn dùng. Dưới đây là các đầu ra bạn thường dùng nhất.

Các kiểu TypeScript

Plugin typescript tạo một kiểu cho mọi object, enum và input trong schema của bạn. Với danh mục khóa học, kiểu Course trông như sau:

export type Course = {
  __typename?: 'Course';
  durationHours: Scalars['Int']['output'];
  id: Scalars['ID']['output'];
  instructor: Instructor;
  level: Level;
  title: Scalars['String']['output'];
};

Các kiểu này mô tả toàn bộ schema, hữu ích phía server, trong dữ liệu test, và trong các hàm tiện ích làm việc với object đầy đủ.

Nhưng chúng không phù hợp cho mã phía client. Kiểu Course nói mọi trường đều tồn tại, ngay cả những trường query của bạn không yêu cầu.

Các kiểu cho operations

Plugin typescript-operations tạo hai kiểu cho mỗi operation: một cho kết quả và một cho biến. Cách hoạt động giống nhau cho query và subscription.

Đây là những gì nó tạo cho mutation EnrollStudent:

export type EnrollStudentMutationVariables = Exact<{
  courseId: string | number;
  studentEmail: string;
}>;

export type EnrollStudentMutation = { enrollStudent: { id: string, course: { title: string } } };

Kiểu kết quả khớp selection set, từng trường một. Kiểu biến là Exact, nên TypeScript sẽ từ chối biến thừa hoặc sai chính tả.

Biến courseId chấp nhận cả string và number vì scalar ID của GraphQL chấp nhận cả hai làm input. Từ v6, kiểu mặc định là string | number, phù hợp cho tình huống phía client.

Typed document nodes

Một DocumentNode thông thường là một query GraphQL đã parse. Nó biết cấu trúc query, nhưng không biết query trả về gì hay nhận biến gì.

Một typed document node đi kèm cả hai kiểu đó. Plugin typed-document-node tạo một node cho mỗi operation:

export const GetCoursesDocument = { /* ... */ } as unknown as DocumentNode<GetCoursesQuery, GetCoursesQueryVariables>;

Theo cách này, bất kỳ hàm nào chấp nhận TypedDocumentNode đều có thể suy ra kiểu kết quả và biến chỉ từ document. Bạn không cần tự truyền generics, và bạn không thể kết hợp query với sai kiểu.

Hầu hết client GraphQL hiện đại chấp nhận typed document nodes, bao gồm Apollo Client, urql và graphql-request. Bạn sẽ thấy cả ba trong phần sau của bài viết.

Tích hợp theo client cụ thể

Một số plugin đi xa hơn và tạo mã cho một client hay framework cụ thể. Ví dụ:

  • typescript-react-apollo: Tạo các hook React có kiểu cho Apollo Client

  • typescript-urql: Tạo các hook và component có kiểu cho urql

  • typescript-react-query: Tạo các hook có kiểu cho TanStack Query

  • typescript-graphql-request: Tạo một SDK có kiểu với một hàm cho mỗi operation

Tất cả đều là plugin cộng đồng. The Guild đã chuyển chúng sang một kho cộng đồng riêng, nên nhịp phát hành khác với các plugin lõi.

Bạn cũng ít cần chúng hơn trước đây. Khi một client chấp nhận typed document node, các hook sinh ra không thêm nhiều bảo đảm kiểu. Ví dụ, tài liệu của Apollo khuyến nghị dùng plugin typescript-operations, tập trung chỉ tạo kiểu và không bao gồm mã runtime bổ sung.

Nhìn chung, bạn nên bắt đầu với types cho operations và typed document nodes. Sau đó, chỉ thêm plugin theo client nếu đội của bạn muốn các hook hoặc hàm SDK sinh sẵn.

GraphQL Codegen với TypeScript

Các kiểu sinh ra chỉ có ý nghĩa nếu chúng bắt được lỗi, nên hãy cố tình tạo lỗi.

Ví dụ này mở rộng dự án danh mục khóa học với một query thứ hai, một helper nhỏ gửi nó tới API, và mã dùng kết quả. Giả định có một server GraphQL triển khai schema đang chạy tại http://localhost:4000/graphql.

Đây là phần schema ví dụ sử dụng:

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
}

Hai chi tiết quan trọng ở đây. Trường course trả về Course không có !, nên là null khi không có khóa học khớp ID. bio của giảng viên cũng có thể null.

Tạo src/operations/course.graphql với một query lấy một khóa học:

query GetCourse($id: ID!) {
  course(id: $id) {
    title
    durationHours
    instructor {
      name
      bio
    }
  }
}

Chạy lại npm run codegen. Codegen thêm hai kiểu mới vào src/generated/graphql.ts:

Nội dung file graphql.ts

Tính nullable từ schema được giữ nguyên. course có thể là null, bio có thể là null, và id là bắt buộc vì query khai báo là ID!.

Giờ tới phần mã ứng dụng. Tạo src/execute.ts với một helper gửi bất kỳ typed document nào tới 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 này không biết gì về khóa học. Nó nhận cả hai kiểu từ document bạn truyền vào, và hàm print() chuyển document về chuỗi query cho request.

Sau đó dùng nó trong 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");
}

Hàm formatCourse() lấy kiểu tham số từ kiểu query được tạo. NonNullable loại bỏ trường hợp null, vì hàm chỉ chạy sau khi đã kiểm tra if.

Ví dụ dùng top-level await, nên đặt "type": "module" trong package.json và chạy với npx tsx src/course-details.ts:

Gọi thành công tới server GraphQL mock

Gọi thành công tới server GraphQL mock

Giờ, giả định bạn có một server GraphQL đang chạy. Đây là server mock tôi dùng:

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");
});

Giờ hãy thử phá vỡ nó. Mở src/course-details.ts và thay mọi thứ bên dưới hàm formatCourse() bằng ba sai lầm mà các kiểu viết tay thường để lọt:

// 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);

Sau đó kiểm tra kiểu dự án:

npx tsc -p .

TypeScript bắt cả ba lỗi trước khi mã chạy:

Các lỗi được TypeScript bắt

Các lỗi được TypeScript bắt

Đó là cách Codegen giảm sai khác giữa frontend và API. Các kiểu đến từ cùng schema mà server dùng, nên chúng không thể mô tả những trường không tồn tại hay bỏ sót các null mà server có thể trả về.

Và lớp bảo vệ không dừng lại sau lần chạy đầu. Giả sử đội backend làm durationHours có thể null. Bạn tạo lại, kiểu đổi thành number | null, và TypeScript chỉ ra mọi dòng giả định là số.

GraphQL Codegen với các GraphQL client

Codegen không gửi request. Đó là nhiệm vụ của GraphQL client của bạn.

Các typed document node được tạo là điểm bàn giao giữa hai bên. Bạn truyền chúng vào client, và client suy ra kiểu. Các ví dụ dưới dùng cùng file src/generated/graphql.ts và API mock từ phần trước, nên hãy giữ npx tsx mock/server.ts chạy ở cửa sổ terminal khác.

GraphQL Codegen với Apollo

Cài đặt Apollo Client và phụ thuộc rxjs của nó:

npm install @apollo/client rxjs

Ví dụ cũng chạy mutation EnrollStudent. Lưu thành src/operations/enroll.graphql:

mutation EnrollStudent($courseId: ID!, $studentEmail: String!) {
  enrollStudent(courseId: $courseId, studentEmail: $studentEmail) {
    id
    course {
      title
    }
  }
}

Tài liệu của Apollo cũng khuyến nghị hai tùy chọn cấu hình cho Codegen, vì Apollo Client luôn bao gồm trường __typename nhưng không thêm vào root types. Thêm chúng vào đích đầu ra trong codegen.ts:

"src/generated/graphql.ts": {
  plugins: ["typescript-operations", "typed-document-node"],
  config: {
    nonOptionalTypename: true,
    skipTypeNameForRoot: true,
  },
},

Với các tùy chọn này, mọi object trong kiểu kết quả được tạo sẽ có trường __typename bắt buộc, như __typename: 'Course'. Điều đó khớp với những gì cache của Apollo trả về.

Chạy lại npm run codegen để phát hiện mutation và cấu hình mới.

Apollo Client chấp nhận typed document nodes trong query() và mutate(). Lưu đoạn này thành 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);

Đối tượng data có kiểu GetCoursesQuery | undefined, và TypeScript kiểm tra variables so với GetCoursesQueryVariables. Mutation hoạt động tương tự với EnrollStudentMutation và các biến của nó.

Chạy với npx tsx src/apollo.ts:

Kết quả Apollo

Kết quả Apollo

Dòng đầu tiên đến từ query, vì đó là khóa học dành cho người mới duy nhất. Dòng thứ hai đến từ mutation, trả về tiêu đề của khóa học mà sinh viên đã ghi danh.

Các hook React như useQuery() và useMutation() cũng suy ra kiểu từ typed document nodes theo cách tương tự.

GraphQL Codegen với urql

Cài đặt gói core của urql:

npm install @urql/core

urql hoạt động với typed document nodes ngay từ đầu. Truyền document làm đối số đầu tiên cho query() trong 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));

Đối số thứ hai có kiểu là biến của query, và result.data có kiểu là kết quả query.

Chạy với npx tsx src/urql.ts:

Kết quả Urql

Kết quả Urql

Các binding cho framework, như useQuery() trong gói urql cho React, cũng chấp nhận typed document nodes.

GraphQL Codegen với các client khác

Bạn không cần một client đầy đủ tính năng để có kết quả được đánh kiểu.

Client graphql-request là một client tối giản gửi một request và trả về dữ liệu. Cài đặt với lệnh sau:

npm install graphql-request

Nó suy ra kiểu từ typed document nodes giống như Apollo và urql. Lưu đoạn này thành 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);

Chạy với npx tsx src/graphql-request.ts:

Kết quả graphql-request

Kết quả graphql-request

Ý tưởng tương tự hoạt động với TanStack Query. Nó không tự gửi request GraphQL, nên bạn gọi graphql-request hoặc helper execute() từ phần trước trong hàm query, và kiểu kết quả sẽ xuyên suốt tới mã của bạn.

Và nếu client của bạn hoàn toàn không hỗ trợ typed document nodes, bạn vẫn có thể dùng các kiểu operation thuần. Import GetCourseQuery và GetCourseQueryVariables và truyền chúng như generics. Cách này thủ công hơn, nhưng các kiểu vẫn đến từ schema.

Cách chạy GraphQL Codegen tự động

Các kiểu sinh ra chỉ hữu ích nếu chúng khớp với schema và operations hiện tại.

Nếu bạn quên chạy lại Codegen sau khi thay đổi, các kiểu sẽ mô tả mã không còn tồn tại. Đó là vấn đề trôi dạt giống như kiểu viết tay, chỉ thêm bước. Cách khắc phục là loại bỏ bước thủ công khỏi quy trình.

npm scripts

Bắt đầu bằng vài script trong package.json, để mọi người trong nhóm chạy Codegen theo cùng một cách:

{
  "scripts": {
    "codegen": "graphql-codegen",
    "codegen:watch": "graphql-codegen --watch",
    "codegen:check": "graphql-codegen --check",
    "prebuild": "npm run codegen",
    "build": "tsc -p ."
  }
}

Phần còn lại của mục này giải thích mục đích của từng lệnh.

Chế độ watch

Chế độ watch chạy lại Codegen mỗi khi file theo dõi thay đổi. Nó cần gói @parcel/watcher, Codegen không cài sẵn cho bạn:

npm install -D @parcel/watcher
npm run codegen:watch

Nếu bạn bỏ qua việc cài đặt, Codegen chạy một lần và in Failed to import @parcel/watcher thay vì theo dõi.

Chế độ watch theo dõi cả operations của bạn và file schema cục bộ. Nếu bạn thêm một trường vào course.graphql, trường mới sẽ xuất hiện trong GetCourseQuery sau một giây. Nếu bạn làm durationHours có thể null trong schema.graphql, kiểu đổi thành number | null, và trình soạn thảo của bạn đánh dấu mọi dòng giả định là số.

Hãy giữ nó chạy trong một terminal bên cạnh server dev trong lúc phát triển.

Tạo trước khi build

npm sẽ chạy script tên prebuild trước build. Với các script trên, npm run build sẽ tạo lại các kiểu trước, rồi mới chạy trình biên dịch TypeScript.

Bằng cách này, build không bao giờ chạy với các kiểu cũ, ngay cả khi ai đó quên chạy Codegen sau khi pull thay đổi.

CI/CD

Cờ --check chạy Codegen ở chế độ dry-run. Nó không ghi file, so sánh những gì sẽ tạo với những gì có trên đĩa và thoát với mã 1 nếu có khác biệt.

Đây là workflow GitHub Actions tối giản sử dụng nó:

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

Bước codegen:check chỉ hợp lý nếu bạn commit file sinh ra vào repo. Nếu không, bỏ bước đó, vì npm run build đã tạo lại mọi thứ qua hook prebuild.

Tạo lại khi schema thay đổi

Operations thay đổi trong repo của bạn, nên watch mode và prebuild xử lý được. Schema thay đổi khó hơn, vì thường đến từ đội khác.

Cách xử lý phụ thuộc vào nơi đặt schema:

  • File schema trong cùng repo: Chế độ watch và CI sẽ phát hiện thay đổi, đây là thiết lập dễ làm việc nhất
  • Schema từ URL: Codegen fetch phiên bản mới nhất ở mỗi lần chạy, nên codegen:check trong CI sẽ fail ngay khi schema trên server không còn khớp với đầu ra đã commit
  • Schema được xuất bản bởi repo khác: Sao chép hoặc tải về như một phần của build, rồi chạy Codegen trên bản sao cục bộ

Tùy chọn URL có một điểm cần lưu ý. Chế độ watch phản ứng với thay đổi file, nên nó không nhận ra khi schema ở xa thay đổi. Bạn chỉ thấy thay đổi đó ở lần chạy thủ công tiếp theo hoặc job CI.

GraphQL Codegen so với các kiểu viết tay

Cả hai cách đều mang lại các kiểu TypeScript cho dữ liệu GraphQL, nhưng có vài khác biệt bạn nên biết.

Các kiểu viết tay

Với kiểu viết tay, bạn đọc schema và tự viết các interface tương ứng.

Điều này cho bạn toàn quyền kiểm soát. Bạn có thể đặt tên kiểu tùy ý, gộp vài operations thành một kiểu dùng chung, hoặc thêm các kiểu trợ giúp mà schema không có.

Nhưng đó là công việc thủ công. Mỗi trường mới hoặc tham số đổi tên nghĩa là bạn phải tìm và cập nhật kiểu tương ứng bằng tay. Và không có gì cảnh báo nếu bạn bỏ sót.

Đó là nguồn gốc của trôi dạt. Interface Course trong phần mở đầu vẫn biên dịch sau khi backend đổi tên title, vì TypeScript chỉ kiểm tra mã của bạn so với các kiểu của bạn, không phải so với API.

Các kiểu được tạo

Các kiểu được tạo đến từ cùng schema mà server dùng, nên chúng được đồng bộ với các định nghĩa GraphQL theo thiết kế. Một thay đổi schema cộng với một lần chạy Codegen sẽ cập nhật mọi kiểu bị ảnh hưởng cùng lúc.

Bạn cũng viết ít mã hơn rất nhiều. Dự án danh mục khóa học có ba operation và không có kiểu viết tay nào cho chúng.

Và bạn gần như miễn phí có được phát triển theo schema. Bạn đổi schema hoặc một operation, tạo lại, và trình biên dịch cho bạn danh sách mọi thứ cần thay đổi.

Nhưng cần nhớ rằng Codegen chỉ phản ánh những gì bạn đưa cho nó.

Nếu schema đánh dấu mọi trường đều có thể null, các kiểu sinh ra sẽ đầy các kiểm tra | null. Nếu một operation lấy 40 trường khi trang chỉ cần năm, kiểu sinh ra sẽ có 40 trường.

Tóm lại nhanh các khác biệt:

  Các kiểu viết tay Các kiểu được tạo
Bảo trì Thủ công cho mỗi thay đổi schema Một lệnh sau mỗi thay đổi
Trôi dạt schema Có thể xảy ra và im lặng Bắt tại lúc tạo hoặc biên dịch
Mã khuôn mẫu Tăng theo mỗi operation Không phải viết tay
Kiểm soát kiểu tùy chỉnh Toàn quyền Giới hạn theo tùy chọn plugin
Phát triển theo schema Đối chiếu thủ công Trình biên dịch chỉ mọi dòng bị ảnh hưởng

So sánh Codegen với kiểu viết tay

GraphQL Codegen so với Introspection của GraphQL

Hai thứ này hay bị nhầm vì Codegen thường dùng introspection. Tuy vậy, chúng giải quyết các vấn đề khác nhau.

Introspection là tính năng của đặc tả GraphQL. Bất kỳ server GraphQL nào cũng có thể trả lời các query về chính schema của nó qua các trường đặc biệt như __schema và __type. Kết quả là mô tả JSON của mọi kiểu, trường và tham số mà API cung cấp.

Nói đơn giản, introspection trả lời câu hỏi "API này trông như thế nào?"

Codegen trả lời câu hỏi "Ứng dụng của tôi cần mã gì?". Nó lấy thông tin schema và operations của bạn, rồi viết các kiểu TypeScript và typed document nodes.

Khi bạn đặt schema là URL trong codegen.ts, Codegen gửi một introspection query tới server đó để lấy schema. Với file .graphql cục bộ, hoàn toàn không có introspection.

Codegen cũng có thể tạo ra kết quả introspection như một đầu ra. Plugin @graphql-codegen/introspection ghi schema thành JSON introspection, mà một số công cụ như Graphcache của urql dùng cho cache nhận thức schema.

  Introspection GraphQL Codegen
Bản chất Hệ thống query tích hợp của GraphQL Công cụ dòng lệnh và plugin
Đầu vào Một query tới server đang chạy Một schema và operations của bạn
Đầu ra Mô tả JSON của schema Types, document nodes, mã client
Thời điểm chạy Bất cứ khi nào client hỏi server Trong quá trình phát triển, build và CI

So sánh Codegen với introspection

Các vấn đề thường gặp với GraphQL Codegen

Hầu hết lỗi của Codegen rơi vào một trong bốn vấn đề sau.

Thiếu các kiểu được tạo

Codegen chạy không lỗi, nhưng kiểu bạn mong đợi không có trong file đầu ra.

Hãy kiểm tra ba điều sau theo thứ tự:

  • Schema: Nếu kiểu không tồn tại trong schema mà Codegen tải, nó không thể được tạo, nên hãy mở file schema hoặc tự chạy introspection và xác nhận nó có ở đó

  • Documents: Các kiểu operation chỉ tồn tại cho những operation Codegen tìm thấy, nên nếu file chứa query của bạn không khớp mẫu glob documents, các kiểu của nó sẽ thiếu

  • Plugins: Từ v6, typescript-operations chỉ tạo enum và input mà operations của bạn dùng, nên nếu bạn cần kiểu đầy đủ của schema như Course, hãy thêm plugin typescript vào một file đầu ra riêng

Nếu bạn dùng một plugin chưa cài, Codegen sẽ lỗi với thông điệp rõ ràng:

Unable to find template plugin matching 'typescript-react-apollo'
Install one of the following packages:
- @graphql-codegen/typescript-react-apollo

Codegen không tìm thấy operations

Lỗi này nghĩa là glob documents không khớp với bất kỳ file nào có operation bên trong:

Unable to find any GraphQL type definitions for the following pointers:
  - src/**/*.gql

Kiểm tra phần mở rộng trong glob có khớp file của bạn. Mẫu .gql sẽ không tìm file .graphql, và mẫu src/**/*.graphql sẽ không tìm operations inline trong file .ts.

Với operations inline, kiểm tra thẻ nữa. Codegen chỉ tìm template literal bọc trong gql hoặc đánh dấu comment /* GraphQL */. Một template string thông thường chứa query sẽ bị bỏ qua.

Nếu bạn đặt ignoreNoDocuments: true, Codegen không hiển thị lỗi này và tạo đầu ra không có các kiểu operation. Tùy chọn đó hữu ích cho dự án hoàn toàn mới, nhưng có thể che giấu một glob bị hỏng về sau.

Các kiểu được tạo đã lỗi thời

Các kiểu biên dịch được, nhưng không phản ánh thay đổi mới nhất đối với một operation hoặc schema.

Codegen không tự chạy. Hãy chạy lại sau mỗi thay đổi với file .graphql, operation inline, schema, hoặc codegen.ts. Trong lúc phát triển, npm run codegen:watch sẽ làm điều đó cho bạn.

Nếu các kiểu bị cũ trong CI, hãy commit các file đã tạo lại hoặc để hook prebuild tạo lại chúng trước khi build.

Các kiểu được tạo không khớp với API

Các kiểu biên dịch được, nhưng dữ liệu từ server có cấu trúc khác khi chạy thực tế.

Hầu như luôn có nghĩa là Codegen và ứng dụng của bạn nói chuyện với các schema khác nhau. Ví dụ, Codegen đọc schema.graphql cục bộ đã một tuần tuổi, còn ứng dụng gọi API production đã thay đổi từ đó.

Kiểm tra khóa schema trỏ đi đâu. Nếu là file, so sánh nó với API mà ứng dụng gọi và cập nhật. Nếu là URL, đảm bảo đó là cùng môi trường mà ứng dụng dùng, không phải server staging có schema mới hơn hoặc cũ hơn.

Sau đó tạo lại và xem diff. Nếu một file operation thay đổi nhưng file sinh ra không đổi, có thể operation nằm ngoài glob documents.

Thực hành tốt nhất cho GraphQL Codegen

Codegen không cần quá nhiều cấu hình để hoạt động tốt. Những thực hành này giúp giữ mọi thứ đơn giản khi dự án lớn dần:

  • Giữ schema và file sinh ra đồng bộ: Chạy chế độ watch trong lúc phát triển và prebuild trước khi build, để không ai làm việc với kiểu từ schema cũ

  • Tạo các kiểu theo từng operation: Dùng typescript-operations cho mã client vì chúng khớp chính xác với những gì mỗi query lấy

  • Chỉ commit file sinh ra khi quy trình làm việc của bạn cần: Commit cho phép bạn review thay đổi kiểu trong pull request và chạy --check trong CI; nếu đội bạn không cần, thêm thư mục đầu ra vào .gitignore và tạo lại ở mỗi lần build

  • Giữ cấu hình ngắn gọn: Một file đầu ra với hai plugin là đủ cho hầu hết dự án client, và mỗi tùy chọn thêm là một thứ mà lập trình viên tiếp theo phải hiểu trước khi thay đổi

  • Chạy Codegen trong CI ở mọi lần push: Một bước --check hoặc hook prebuild sẽ bắt các thay đổi ai đó quên tạo lại cục bộ

  • Không bao giờ chỉnh sửa file sinh ra bằng tay: Lần chạy Codegen tiếp theo sẽ ghi đè thay đổi của bạn, vì vậy hãy sửa schema, operation, hoặc cấu hình thay vì sửa trực tiếp

Kết luận

GraphQL Codegen đảm nhiệm phần lặp lại nhất của phát triển GraphQL — chuyển schema và operations thành các kiểu TypeScript thay vì viết tay.

Quy trình làm việc vẫn như nhau dù dự án lớn tới đâu. Bạn định nghĩa schema và operations, cấu hình Codegen, tạo ra các kiểu và typed document nodes, rồi import chúng vào mã ứng dụng.

Giá trị lớn nhất là các kiểu của bạn luôn mô tả API mà bạn thực sự gọi, không phải phần mã bạn không còn phải viết. Khi schema thay đổi, trình biên dịch sẽ cho bạn biết nếu có gì đó hỏng để bạn bắt lỗi sớm.

Nếu bạn muốn tìm hiểu cách GraphQL được dùng trong một cơ sở dữ liệu NoSQL hiện đại, hãy đọc bài viết MongoDB và GraphQL của chúng tôi.


Dario Radečić's photo
Author
Dario Radečić
LinkedIn
Chuyên gia Khoa học Dữ liệu Cấp cao, làm việc tại Croatia. Tác giả Công nghệ Hàng đầu với hơn 700 bài viết đã xuất bản, đạt trên 10 triệu lượt xem. Tác giả cuốn sách Tự động hóa Machine Learning với TPOT.

Câu hỏi thường gặp về GraphQL Codegen

GraphQL Codegen được dùng để làm gì?

GraphQL Codegen tạo mã từ một schema GraphQL và các operation GraphQL của bạn. Hầu hết các nhóm dùng nó để có các kiểu TypeScript và typed document nodes cho query và mutation, nhờ đó họ không phải viết và cập nhật các kiểu đó bằng tay. Nó cũng có thể tạo kiểu cho resolver phía server và các hook dựng sẵn cho client cụ thể.

Tôi có cần một server GraphQL đang chạy để dùng Codegen không?

Không. Codegen có thể đọc schema từ file .graphql cục bộ, nên hoạt động mà không cần server nào. Bạn chỉ cần server đang chạy nếu bạn trỏ khóa schema tới URL, vì khi đó Codegen sẽ fetch schema qua introspection.

Tôi có nên commit các file sinh ra vào Git không?

Tùy quy trình làm việc của nhóm bạn. Việc commit file sinh ra cho phép bạn review thay đổi kiểu trong pull request và dùng graphql-codegen --check trong CI để bắt đầu ra cũ. Nếu không cần, hãy thêm thư mục đầu ra vào .gitignore và tạo lại file trước mỗi lần build.

Tôi nên dùng preset client hay các plugin riêng lẻ?

The Guild khuyến nghị preset client cho hầu hết ứng dụng client, vì nó gộp types cho operations, typed document nodes và fragment masking trong một thiết lập. Apollo khuyến nghị plugin typescript-operations thay thế, vì preset thêm mã runtime và tính năng xung đột với Apollo Client. Nếu bạn dùng Apollo, hãy chọn các plugin. Nếu không, preset là mặc định tốt.

Vì sao tôi gặp lỗi khai báo trùng lặp sau khi cập nhật Codegen?

Từ v6, plugin typescript-operations tự tạo các enum và input. Nếu cấu hình của bạn vẫn liệt kê cả typescript và typescript-operations cho cùng một file đầu ra, cả hai plugin sẽ khai báo các kiểu như Level, và TypeScript sẽ ném lỗi khai báo trùng lặp. Hãy gỡ plugin typescript khỏi đầu ra đó, hoặc chuyển nó sang file riêng nếu bạn vẫn cần các kiểu đầy đủ cho schema.

Chủ đề

Học cùng DataCamp

{count, plural, one {Khóa học} other {Khóa học}}

Software Development with Claude Code

{hours} giờ
8.9K
Claude Code brings AI assistance to your terminal. Learn the workflows that turn it into a reliable tool for real software development.
Xem chi tiếtRight Arrow
Bắt Đầu Khóa Học
Xem thêmRight Arrow