graphql

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GraphQL Skill

GraphQL 技能

Summary

概述

GraphQL is a query language and runtime for APIs that enables clients to request exactly the data they need. It provides a strongly-typed schema, single endpoint architecture, and eliminates over-fetching/under-fetching problems common in REST APIs.
GraphQL是用于API的查询语言和运行时,允许客户端精准请求所需的数据。它提供强类型模式、单端点架构,解决了REST API中常见的过度获取/获取不足数据的问题。

When to Use

适用场景

  • Building flexible APIs for multiple client types (web, mobile, IoT)
  • Complex data requirements with nested relationships
  • Mobile-first applications needing bandwidth efficiency
  • Reducing API versioning complexity
  • Real-time data with subscriptions
  • Microservices aggregation and federation
  • Developer experience with strong typing and introspection
  • 为多种客户端类型(Web、移动设备、IoT)构建灵活的API
  • 具有嵌套关系的复杂数据需求
  • 需要带宽效率的移动优先应用
  • 降低API版本管理复杂度
  • 支持实时数据的订阅场景
  • 微服务聚合与联邦
  • 具备强类型和自省能力的开发者体验

Quick Start

快速开始

1. Define Schema (SDL)

1. 定义Schema(SDL)

graphql
undefined
graphql
undefined

schema.graphql

schema.graphql

type User { id: ID! name: String! email: String! posts: [Post!]! }
type Post { id: ID! title: String! content: String! author: User! publishedAt: DateTime }
type Query { user(id: ID!): User users: [User!]! post(id: ID!): Post }
type Mutation { createPost(title: String!, content: String!, authorId: ID!): Post! updatePost(id: ID!, title: String, content: String): Post! deletePost(id: ID!): Boolean! }
undefined
type User { id: ID! name: String! email: String! posts: [Post!]! }
type Post { id: ID! title: String! content: String! author: User! publishedAt: DateTime }
type Query { user(id: ID!): User users: [User!]! post(id: ID!): Post }
type Mutation { createPost(title: String!, content: String!, authorId: ID!): Post! updatePost(id: ID!, title: String, content: String): Post! deletePost(id: ID!): Boolean! }
undefined

2. Write Resolvers (TypeScript + Apollo Server)

2. 编写解析器(TypeScript + Apollo Server)

typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { readFileSync } from 'fs';

// Load schema
const typeDefs = readFileSync('./schema.graphql', 'utf-8');

// Mock data
const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
];

const posts = [
  { id: '1', title: 'GraphQL Intro', content: 'Learning GraphQL...', authorId: '1' },
  { id: '2', title: 'Apollo Server', content: 'Building APIs...', authorId: '1' },
];

// Resolvers
const resolvers = {
  Query: {
    user: (_, { id }) => users.find(u => u.id === id),
    users: () => users,
    post: (_, { id }) => posts.find(p => p.id === id),
  },

  Mutation: {
    createPost: (_, { title, content, authorId }) => {
      const post = {
        id: String(posts.length + 1),
        title,
        content,
        authorId,
      };
      posts.push(post);
      return post;
    },

    updatePost: (_, { id, title, content }) => {
      const post = posts.find(p => p.id === id);
      if (!post) throw new Error('Post not found');
      if (title) post.title = title;
      if (content) post.content = content;
      return post;
    },

    deletePost: (_, { id }) => {
      const index = posts.findIndex(p => p.id === id);
      if (index === -1) return false;
      posts.splice(index, 1);
      return true;
    },
  },

  User: {
    posts: (user) => posts.filter(p => p.authorId === user.id),
  },

  Post: {
    author: (post) => users.find(u => u.id === post.authorId),
  },
};

// Create server
const server = new ApolloServer({ typeDefs, resolvers });

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});
typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { readFileSync } from 'fs';

// Load schema
const typeDefs = readFileSync('./schema.graphql', 'utf-8');

// Mock data
const users = [
  { id: '1', name: 'Alice', email: 'alice@example.com' },
  { id: '2', name: 'Bob', email: 'bob@example.com' },
];

const posts = [
  { id: '1', title: 'GraphQL Intro', content: 'Learning GraphQL...', authorId: '1' },
  { id: '2', title: 'Apollo Server', content: 'Building APIs...', authorId: '1' },
];

// Resolvers
const resolvers = {
  Query: {
    user: (_, { id }) => users.find(u => u.id === id),
    users: () => users,
    post: (_, { id }) => posts.find(p => p.id === id),
  },

  Mutation: {
    createPost: (_, { title, content, authorId }) => {
      const post = {
        id: String(posts.length + 1),
        title,
        content,
        authorId,
      };
      posts.push(post);
      return post;
    },

    updatePost: (_, { id, title, content }) => {
      const post = posts.find(p => p.id === id);
      if (!post) throw new Error('Post not found');
      if (title) post.title = title;
      if (content) post.content = content;
      return post;
    },

    deletePost: (_, { id }) => {
      const index = posts.findIndex(p => p.id === id);
      if (index === -1) return false;
      posts.splice(index, 1);
      return true;
    },
  },

  User: {
    posts: (user) => posts.filter(p => p.authorId === user.id),
  },

  Post: {
    author: (post) => users.find(u => u.id === post.authorId),
  },
};

// Create server
const server = new ApolloServer({ typeDefs, resolvers });

startStandaloneServer(server, {
  listen: { port: 4000 },
}).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

3. Query Data (Client)

3. 查询数据(客户端)

typescript
// Using Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
});

// Query
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
        publishedAt
      }
    }
  }
`;

const { data } = await client.query({
  query: GET_USER,
  variables: { id: '1' },
});

// Mutation
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
    createPost(title: $title, content: $content, authorId: $authorId) {
      id
      title
      content
    }
  }
`;

const { data: postData } = await client.mutate({
  mutation: CREATE_POST,
  variables: {
    title: 'New Post',
    content: 'Hello GraphQL!',
    authorId: '1',
  },
});

typescript
// Using Apollo Client
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000',
  cache: new InMemoryCache(),
});

// Query
const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
      posts {
        id
        title
        publishedAt
      }
    }
  }
`;

const { data } = await client.query({
  query: GET_USER,
  variables: { id: '1' },
});

// Mutation
const CREATE_POST = gql`
  mutation CreatePost($title: String!, $content: String!, $authorId: ID!) {
    createPost(title: $title, content: $content, authorId: $authorId) {
      id
      title
      content
    }
  }
`;

const { data: postData } = await client.mutate({
  mutation: CREATE_POST,
  variables: {
    title: 'New Post',
    content: 'Hello GraphQL!',
    authorId: '1',
  },
});

Core Concepts

核心概念

GraphQL Fundamentals

GraphQL 核心基础

  • Schema-First Design: Define API contract with Schema Definition Language (SDL)
  • Type Safety: Strongly-typed schema enforced at runtime and build-time
  • Single Endpoint: All queries and mutations go through one URL (e.g.,
    /graphql
    )
  • Client-Specified Queries: Clients request exactly what they need
  • Hierarchical Data: Queries mirror the shape of returned data
  • Introspection: Schema is self-documenting and queryable
  • Schema优先设计:使用Schema定义语言(SDL)定义API契约
  • 类型安全:强类型模式在运行时和构建时都能得到强制执行
  • 单端点:所有查询和变更都通过一个URL(如
    /graphql
    )处理
  • 客户端指定查询:客户端精准请求所需的数据
  • 分层数据:查询结构与返回数据的结构一致
  • 自省能力:Schema是自文档化且可查询的

Operations

操作类型

graphql
undefined
graphql
undefined

Query - Read data (GET-like)

Query - 读取数据(类似GET)

query GetUser { user(id: "1") { name } }
query GetUser { user(id: "1") { name } }

Mutation - Modify data (POST/PUT/DELETE-like)

Mutation - 修改数据(类似POST/PUT/DELETE)

mutation CreateUser { createUser(name: "Alice", email: "alice@example.com") { id name } }
mutation CreateUser { createUser(name: "Alice", email: "alice@example.com") { id name } }

Subscription - Real-time updates (WebSocket)

Subscription - 实时更新(WebSocket)

subscription OnPostCreated { postCreated { id title author { name } } }
undefined
subscription OnPostCreated { postCreated { id title author { name } } }
undefined

Fields and Arguments

字段与参数

graphql
type Query {
  # Field with arguments
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!

  # Search with multiple arguments
  searchPosts(
    query: String!
    category: String
    limit: Int = 20
  ): [Post!]!
}

graphql
type Query {
  # 带参数的字段
  user(id: ID!): User
  users(limit: Int = 10, offset: Int = 0): [User!]!

  # 多参数搜索
  searchPosts(
    query: String!
    category: String
    limit: Int = 20
  ): [Post!]!
}

Schema Definition Language (SDL)

Schema定义语言(SDL)

Basic Type Definition

基础类型定义

graphql
type User {
  id: ID!              # Non-null ID scalar
  name: String!        # Non-null String
  email: String!
  age: Int             # Nullable Int
  isActive: Boolean!
  posts: [Post!]!      # Non-null list of non-null Posts
  profile: Profile     # Nullable object type
}

type Profile {
  bio: String
  avatarUrl: String
  website: String
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  tags: [String!]      # Non-null list, nullable elements
  publishedAt: DateTime
}
graphql
type User {
  id: ID!              # 非空ID标量类型
  name: String!        # 非空字符串
  email: String!
  age: Int             # 可空整数
  isActive: Boolean!
  posts: [Post!]!      # 非空列表,包含非空Post类型
  profile: Profile     # 可空对象类型
}

type Profile {
  bio: String
  avatarUrl: String
  website: String
}

type Post {
  id: ID!
  title: String!
  content: String!
  author: User!
  tags: [String!]      # 非空列表,元素可空
  publishedAt: DateTime
}

Input Types (for mutations)

输入类型(用于变更)

graphql
input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

input UpdateUserInput {
  name: String
  email: String
  age: Int
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}
graphql
input CreateUserInput {
  name: String!
  email: String!
  age: Int
}

input UpdateUserInput {
  name: String
  email: String
  age: Int
}

type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
}

Interfaces

接口

graphql
interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type User implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
  email: String!
}

type Post implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  content: String!
}

type Query {
  node(id: ID!): Node  # Can return User or Post
}
graphql
interface Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
}

type User implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  name: String!
  email: String!
}

type Post implements Node {
  id: ID!
  createdAt: DateTime!
  updatedAt: DateTime!
  title: String!
  content: String!
}

type Query {
  node(id: ID!): Node  # 可返回User或Post类型
}

Unions

联合类型

graphql
union SearchResult = User | Post | Comment

type Query {
  search(query: String!): [SearchResult!]!
}
graphql
union SearchResult = User | Post | Comment

type Query {
  search(query: String!): [SearchResult!]!
}

Client query with fragments

客户端查询使用片段

query Search { search(query: "graphql") { ... on User { name email } ... on Post { title content } ... on Comment { text author { name } } } }
undefined
query Search { search(query: "graphql") { ... on User { name email } ... on Post { title content } ... on Comment { text author { name } } } }
undefined

Enums

枚举类型

graphql
enum Role {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type User {
  id: ID!
  name: String!
  role: Role!
}

type Post {
  id: ID!
  title: String!
  status: PostStatus!
}

graphql
enum Role {
  ADMIN
  MODERATOR
  USER
  GUEST
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type User {
  id: ID!
  name: String!
  role: Role!
}

type Post {
  id: ID!
  title: String!
  status: PostStatus!
}

Type System

类型系统

Scalar Types

标量类型

graphql
undefined
graphql
undefined

Built-in scalars

内置标量类型

scalar Int # Signed 32-bit integer scalar Float # Signed double-precision floating-point scalar String # UTF-8 character sequence scalar Boolean # true or false scalar ID # Unique identifier (serialized as String)
scalar Int # 有符号32位整数 scalar Float # 有符号双精度浮点数 scalar String # UTF-8字符序列 scalar Boolean # 布尔值(true或false) scalar ID # 唯一标识符(序列化为字符串)

Custom scalars

自定义标量类型

scalar DateTime # ISO 8601 timestamp scalar Email # Email address scalar URL # Valid URL scalar JSON # Arbitrary JSON scalar Upload # File upload
undefined
scalar DateTime # ISO 8601时间戳 scalar Email # 邮箱地址 scalar URL # 有效URL scalar JSON # 任意JSON数据 scalar Upload # 文件上传
undefined

Custom Scalar Implementation

自定义标量实现

typescript
// DateTime scalar (TypeScript)
import { GraphQLScalarType, Kind } from 'graphql';

const DateTimeScalar = new GraphQLScalarType({
  name: 'DateTime',
  description: 'ISO 8601 DateTime',

  // Serialize to client (output)
  serialize(value: Date) {
    return value.toISOString();
  },

  // Parse from client (input)
  parseValue(value: string) {
    return new Date(value);
  },

  // Parse from query literal
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    return null;
  },
});

// Add to resolvers
const resolvers = {
  DateTime: DateTimeScalar,
  // ... other resolvers
};
typescript
// DateTime scalar (TypeScript)
import { GraphQLScalarType, Kind } from 'graphql';

const DateTimeScalar = new GraphQLScalarType({
  name: 'DateTime',
  description: 'ISO 8601 DateTime',

  // 序列化给客户端(输出)
  serialize(value: Date) {
    return value.toISOString();
  },

  // 解析客户端输入
  parseValue(value: string) {
    return new Date(value);
  },

  // 解析查询字面量
  parseLiteral(ast) {
    if (ast.kind === Kind.STRING) {
      return new Date(ast.value);
    }
    return null;
  },
});

// 添加到解析器
const resolvers = {
  DateTime: DateTimeScalar,
  // ... 其他解析器
};

Non-Null and Lists

非空与列表

graphql
type User {
  name: String!           # Non-null String
  email: String           # Nullable String

  tags: [String!]!        # Non-null list of non-null Strings
  friends: [User!]        # Nullable list of non-null Users
  posts: [Post]!          # Non-null list of nullable Posts
  comments: [Comment]     # Nullable list of nullable Comments
}

graphql
type User {
  name: String!           # 非空字符串
  email: String           # 可空字符串

  tags: [String!]!        # 非空列表,包含非空字符串
  friends: [User!]        # 可空列表,包含非空User类型
  posts: [Post]!          # 非空列表,包含可空Post类型
  comments: [Comment]     # 可空列表,包含可空Comment类型
}

Queries and Mutations

查询与变更

Query Variables

查询变量

typescript
// Define query with variables
const GET_USER = gql`
  query GetUser($id: ID!, $includePosts: Boolean = false) {
    user(id: $id) {
      id
      name
      email
      posts @include(if: $includePosts) {
        id
        title
      }
    }
  }
`;

// Execute with variables
const { data } = await client.query({
  query: GET_USER,
  variables: {
    id: '1',
    includePosts: true,
  },
});
typescript
// 定义带变量的查询
const GET_USER = gql`
  query GetUser($id: ID!, $includePosts: Boolean = false) {
    user(id: $id) {
      id
      name
      email
      posts @include(if: $includePosts) {
        id
        title
      }
    }
  }
`;

// 传入变量执行查询
const { data } = await client.query({
  query: GET_USER,
  variables: {
    id: '1',
    includePosts: true,
  },
});

Aliases

别名

graphql
query {
  # Fetch same field with different arguments
  user1: user(id: "1") {
    name
  }
  user2: user(id: "2") {
    name
  }

  # Alias for clarity
  currentUser: me {
    id
    name
  }
}
graphql
query {
  # 使用不同参数获取同一个字段
  user1: user(id: "1") {
    name
  }
  user2: user(id: "2") {
    name
  }

  # 使用别名提高可读性
  currentUser: me {
    id
    name
  }
}

Fragments

片段

graphql
undefined
graphql
undefined

Define reusable fragment

定义可复用的片段

fragment UserFields on User { id name email createdAt }
fragment PostSummary on Post { id title publishedAt author { ...UserFields } }
fragment UserFields on User { id name email createdAt }
fragment PostSummary on Post { id title publishedAt author { ...UserFields } }

Use fragments in query

在查询中使用片段

query { user(id: "1") { ...UserFields posts { ...PostSummary } } }
undefined
query { user(id: "1") { ...UserFields posts { ...PostSummary } } }
undefined

Directives

指令

graphql
undefined
graphql
undefined

Built-in directives

内置指令

query GetUser($id: ID!, $withPosts: Boolean!, $skipEmail: Boolean!) { user(id: $id) { name email @skip(if: $skipEmail) posts @include(if: $withPosts) { title } } }
query GetUser($id: ID!, $withPosts: Boolean!, $skipEmail: Boolean!) { user(id: $id) { name email @skip(if: $skipEmail) posts @include(if: $withPosts) { title } } }

Custom directive definition

自定义指令定义

directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query { users: [User!]! @auth(requires: ADMIN) me: User! @auth }
undefined
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query { users: [User!]! @auth(requires: ADMIN) me: User! @auth }
undefined

Mutations Best Practices

变更最佳实践

graphql
undefined
graphql
undefined

Single mutation with input type

带输入类型的单一变更

type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload! deletePost(id: ID!): DeletePostPayload! }
input CreatePostInput { title: String! content: String! categoryId: ID! tags: [String!] }
type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload! deletePost(id: ID!): DeletePostPayload! }
input CreatePostInput { title: String! content: String! categoryId: ID! tags: [String!] }

Payload pattern for mutations

变更的负载模式

type CreatePostPayload { post: Post # Created resource userErrors: [UserError!]! # Client errors success: Boolean! }
type UserError { field: String! # Which field caused error message: String! # Human-readable message }

---
type CreatePostPayload { post: Post # 创建的资源 userErrors: [UserError!]! # 客户端错误 success: Boolean! }
type UserError { field: String! # 导致错误的字段 message: String! # 人类可读的错误信息 }

---

Resolvers and DataLoaders

解析器与DataLoader

Resolver Signature

解析器签名

typescript
type Resolver<TParent, TArgs, TContext, TResult> = (
  parent: TParent,      // Parent object
  args: TArgs,          // Field arguments
  context: TContext,    // Shared context (auth, db, etc.)
  info: GraphQLResolveInfo  // Query metadata
) => TResult | Promise<TResult>;
typescript
type Resolver<TParent, TArgs, TContext, TResult> = (
  parent: TParent,      # 父对象
  args: TArgs,          # 字段参数
  context: TContext,    # 共享上下文(认证、数据库等)
  info: GraphQLResolveInfo  # 查询元数据
) => TResult | Promise<TResult>;

Basic Resolvers

基础解析器

typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => {
      return db.users.findById(id);
    },

    users: async (_, { limit = 10, offset = 0 }, { db }) => {
      return db.users.findMany({ limit, offset });
    },
  },

  Mutation: {
    createUser: async (_, { input }, { db, userId }) => {
      if (!userId) {
        throw new Error('Authentication required');
      }

      const user = await db.users.create(input);
      return { user, userErrors: [], success: true };
    },
  },

  User: {
    // Field resolver - only called if client requests 'posts'
    posts: async (user, _, { db }) => {
      return db.posts.findByAuthorId(user.id);
    },

    // Computed field
    fullName: (user) => {
      return `${user.firstName} ${user.lastName}`;
    },
  },
};
typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => {
      return db.users.findById(id);
    },

    users: async (_, { limit = 10, offset = 0 }, { db }) => {
      return db.users.findMany({ limit, offset });
    },
  },

  Mutation: {
    createUser: async (_, { input }, { db, userId }) => {
      if (!userId) {
        throw new Error('Authentication required');
      }

      const user = await db.users.create(input);
      return { user, userErrors: [], success: true };
    },
  },

  User: {
    # 字段解析器 - 仅当客户端请求'posts'时才会调用
    posts: async (user, _, { db }) => {
      return db.posts.findByAuthorId(user.id);
    },

    # 计算字段
    fullName: (user) => {
      return `${user.firstName} ${user.lastName}`;
    },
  },
};

The N+1 Problem

N+1问题

typescript
// ❌ BAD - N+1 queries
const resolvers = {
  Query: {
    users: () => db.users.findMany(), // 1 query
  },
  User: {
    // Called for EACH user - N queries!
    posts: (user) => db.posts.findByAuthorId(user.id),
  },
};

// Querying 100 users = 1 + 100 = 101 database queries!
typescript
// ❌ 糟糕的实现 - N+1查询
const resolvers = {
  Query: {
    users: () => db.users.findMany(), // 1次查询
  },
  User: {
    # 对每个用户都会调用 - N次查询!
    posts: (user) => db.posts.findByAuthorId(user.id),
  },
};

DataLoader Solution

查询100个用户 = 1 + 100 = 101次数据库查询!

typescript
import DataLoader from 'dataloader';

// Batch function - receives array of keys
async function batchLoadPosts(authorIds: string[]) {
  const posts = await db.posts.findByAuthorIds(authorIds);

  // Group by author ID
  const postsByAuthor = authorIds.map(authorId =>
    posts.filter(post => post.authorId === authorId)
  );

  return postsByAuthor;
}

// Create context with loaders
function createContext({ req }) {
  return {
    db,
    userId: req.userId,
    loaders: {
      posts: new DataLoader(batchLoadPosts),
    },
  };
}

// ✅ GOOD - Batched queries
const resolvers = {
  Query: {
    users: () => db.users.findMany(), // 1 query
  },
  User: {
    // Uses DataLoader - batches all requests into 1 query!
    posts: (user, _, { loaders }) => {
      return loaders.posts.load(user.id);
    },
  },
};

// Querying 100 users = 1 + 1 = 2 database queries!
undefined

Advanced DataLoader Patterns

DataLoader解决方案

typescript
// DataLoader with caching
const userLoader = new DataLoader(
  async (ids) => {
    const users = await db.users.findByIds(ids);
    return ids.map(id => users.find(u => u.id === id));
  },
  {
    cache: true,           // Enable caching (default)
    maxBatchSize: 100,     // Limit batch size
    batchScheduleFn: (cb) => setTimeout(cb, 10), // Debounce batching
  }
);

// Cache manipulation
userLoader.clear(id);              // Clear single key
userLoader.clearAll();             // Clear entire cache
userLoader.prime(id, user);        // Prime cache with value

typescript
import DataLoader from 'dataloader';

Subscriptions

批量函数 - 接收键数组

WebSocket Setup (Apollo Server)

typescript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';

const app = express();
const httpServer = createServer(app);

const schema = makeExecutableSchema({ typeDefs, resolvers });

// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
  server: httpServer,
  path: '/graphql',
});

const serverCleanup = useServer({ schema }, wsServer);

const server = new ApolloServer({
  schema,
  plugins: [
    ApolloServerPluginDrainHttpServer({ httpServer }),
    {
      async serverWillStart() {
        return {
          async drainServer() {
            await serverCleanup.dispose();
          },
        };
      },
    },
  ],
});

await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));

httpServer.listen(4000);
async function batchLoadPosts(authorIds: string[]) { const posts = await db.posts.findByAuthorIds(authorIds);

按作者ID分组

const postsByAuthor = authorIds.map(authorId => posts.filter(post => post.authorId === authorId) );
return postsByAuthor; }

Subscription Schema

创建带加载器的上下文

graphql
type Subscription {
  postCreated: Post!
  postUpdated(id: ID!): Post!
  messageAdded(channelId: ID!): Message!
  userStatusChanged(userId: ID!): UserStatus!
}

type Message {
  id: ID!
  text: String!
  author: User!
  channelId: ID!
  createdAt: DateTime!
}

enum UserStatus {
  ONLINE
  OFFLINE
  AWAY
}
function createContext({ req }) { return { db, userId: req.userId, loaders: { posts: new DataLoader(batchLoadPosts), }, }; }

Subscription Resolvers (PubSub)

✅ 良好的实现 - 批量查询

typescript
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const resolvers = {
  Mutation: {
    createPost: async (_, { input }, { db }) => {
      const post = await db.posts.create(input);

      // Publish to subscribers
      pubsub.publish('POST_CREATED', { postCreated: post });

      return { post, success: true, userErrors: [] };
    },

    sendMessage: async (_, { channelId, text }, { db, userId }) => {
      const message = await db.messages.create({
        channelId,
        text,
        authorId: userId,
      });

      pubsub.publish(`MESSAGE_${channelId}`, {
        messageAdded: message,
      });

      return message;
    },
  },

  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
    },

    postUpdated: {
      subscribe: (_, { id }) => pubsub.asyncIterator([`POST_UPDATED_${id}`]),
    },

    messageAdded: {
      subscribe: (_, { channelId }) => {
        return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
      },
    },
  },
};
const resolvers = { Query: { users: () => db.users.findMany(), // 1次查询 }, User: { # 使用DataLoader - 将所有请求批量处理为1次查询! posts: (user, _, { loaders }) => { return loaders.posts.load(user.id); }, }, };

Client Subscriptions (Apollo Client)

查询100个用户 = 1 + 1 = 2次数据库查询!

typescript
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

// HTTP link for queries and mutations
const httpLink = new HttpLink({
  uri: 'http://localhost:4000/graphql',
});

// WebSocket link for subscriptions
const wsLink = new GraphQLWsLink(
  createClient({
    url: 'ws://localhost:4000/graphql',
  })
);

// Split based on operation type
const splitLink = split(
  ({ query }) => {
    const definition = getMainDefinition(query);
    return (
      definition.kind === 'OperationDefinition' &&
      definition.operation === 'subscription'
    );
  },
  wsLink,
  httpLink
);

const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

// Use subscription
const MESSAGES_SUBSCRIPTION = gql`
  subscription OnMessageAdded($channelId: ID!) {
    messageAdded(channelId: $channelId) {
      id
      text
      author {
        name
      }
      createdAt
    }
  }
`;

function ChatComponent({ channelId }) {
  const { data, loading } = useSubscription(MESSAGES_SUBSCRIPTION, {
    variables: { channelId },
  });

  if (loading) return <p>Loading...</p>;

  return <div>New message: {data.messageAdded.text}</div>;
}
undefined

Redis PubSub (Production)

高级DataLoader模式

typescript
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000),
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

// Use same as in-memory PubSub
pubsub.publish('POST_CREATED', { postCreated: post });
pubsub.asyncIterator(['POST_CREATED']);

typescript
undefined

Error Handling

带缓存的DataLoader

Error Types

typescript
import { GraphQLError } from 'graphql';

// Custom error classes
class AuthenticationError extends GraphQLError {
  constructor(message: string) {
    super(message, {
      extensions: {
        code: 'UNAUTHENTICATED',
        http: { status: 401 },
      },
    });
  }
}

class ForbiddenError extends GraphQLError {
  constructor(message: string) {
    super(message, {
      extensions: {
        code: 'FORBIDDEN',
        http: { status: 403 },
      },
    });
  }
}

class ValidationError extends GraphQLError {
  constructor(message: string, invalidFields: Record<string, string>) {
    super(message, {
      extensions: {
        code: 'BAD_USER_INPUT',
        invalidFields,
      },
    });
  }
}
const userLoader = new DataLoader( async (ids) => { const users = await db.users.findByIds(ids); return ids.map(id => users.find(u => u.id === id)); }, { cache: true, # 启用缓存(默认) maxBatchSize: 100, # 限制批量大小 batchScheduleFn: (cb) => setTimeout(cb, 10), # 防抖批量处理 } );

Throwing Errors in Resolvers

缓存操作

typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { db, userId }) => {
      if (!userId) {
        throw new AuthenticationError('Must be logged in');
      }

      const user = await db.users.findById(id);
      if (!user) {
        throw new GraphQLError('User not found', {
          extensions: { code: 'NOT_FOUND' },
        });
      }

      return user;
    },
  },

  Mutation: {
    createPost: async (_, { input }, { db, userId }) => {
      const errors: Record<string, string> = {};

      if (!input.title || input.title.length < 3) {
        errors.title = 'Title must be at least 3 characters';
      }

      if (!input.content) {
        errors.content = 'Content is required';
      }

      if (Object.keys(errors).length > 0) {
        throw new ValidationError('Invalid input', errors);
      }

      const post = await db.posts.create({ ...input, authorId: userId });
      return { post, success: true, userErrors: [] };
    },
  },
};
userLoader.clear(id); # 清除单个键的缓存 userLoader.clearAll(); # 清除所有缓存 userLoader.prime(id, user); # 预填充缓存值

---

Error Response Format

订阅

WebSocket配置(Apollo Server)

json
{
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "user": null
  }
}
typescript
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import express from 'express';

const app = express();
const httpServer = createServer(app);

const schema = makeExecutableSchema({ typeDefs, resolvers });

Field-Level Error Handling

WebSocket服务器用于订阅

typescript
// Nullable fields allow partial results
type Query {
  user(id: ID!): User        # null on error
  users: [User!]!            # throws on error
  post(id: ID!): Post        # null on error
}

// Resolver can return null instead of throwing
const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => {
      try {
        return await db.users.findById(id);
      } catch (error) {
        console.error('Failed to fetch user:', error);
        return null;  // Returns null instead of error
      }
    },
  },
};

const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', });
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); }, }; }, }, ], });
await server.start(); app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000);
undefined

Schema Design Patterns

订阅Schema

Relay Cursor Connections

graphql
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}
graphql
type Subscription {
  postCreated: Post!
  postUpdated(id: ID!): Post!
  messageAdded(channelId: ID!): Message!
  userStatusChanged(userId: ID!): UserStatus!
}

type Message {
  id: ID!
  text: String!
  author: User!
  channelId: ID!
  createdAt: DateTime!
}

enum UserStatus {
  ONLINE
  OFFLINE
  AWAY
}

Relay Connection Resolver

订阅解析器(PubSub)

typescript
import { fromGlobalId, toGlobalId } from 'graphql-relay';

function encodeCursor(id: string): string {
  return Buffer.from(`cursor:${id}`).toString('base64');
}

function decodeCursor(cursor: string): string {
  return Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
}

const resolvers = {
  Query: {
    posts: async (_, { first = 10, after }, { db }) => {
      const startId = after ? decodeCursor(after) : null;

      // Fetch first + 1 to determine hasNextPage
      const posts = await db.posts.findMany({
        where: startId ? { id: { gt: startId } } : {},
        take: first + 1,
        orderBy: { createdAt: 'desc' },
      });

      const hasNextPage = posts.length > first;
      const nodes = hasNextPage ? posts.slice(0, -1) : posts;

      const edges = nodes.map(node => ({
        node,
        cursor: encodeCursor(node.id),
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: await db.posts.count(),
      };
    },
  },
};
typescript
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const resolvers = {
  Mutation: {
    createPost: async (_, { input }, { db }) => {
      const post = await db.posts.create(input);

      # 发布给订阅者
      pubsub.publish('POST_CREATED', { postCreated: post });

      return { post, success: true, userErrors: [] };
    },

    sendMessage: async (_, { channelId, text }, { db, userId }) => {
      const message = await db.messages.create({
        channelId,
        text,
        authorId: userId,
      });

      pubsub.publish(`MESSAGE_${channelId}`, {
        messageAdded: message,
      });

      return message;
    },
  },

  Subscription: {
    postCreated: {
      subscribe: () => pubsub.asyncIterator(['POST_CREATED']),
    },

    postUpdated: {
      subscribe: (_, { id }) => pubsub.asyncIterator([`POST_UPDATED_${id}`]),
    },

    messageAdded: {
      subscribe: (_, { channelId }) => {
        return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
      },
    },
  },
};

Offset Pagination (Simpler)

客户端订阅(Apollo Client)

graphql
type PostsResponse {
  posts: [Post!]!
  total: Int!
  hasMore: Boolean!
}

type Query {
  posts(limit: Int = 10, offset: Int = 0): PostsResponse!
}
typescript
import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client';
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
import { getMainDefinition } from '@apollo/client/utilities';
import { createClient } from 'graphql-ws';

Global Object Identification (Relay)

HTTP链接用于查询和变更

graphql
interface Node {
  id: ID!  # Global unique ID
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

type Query {
  node(id: ID!): Node
}
typescript
const resolvers = {
  Query: {
    node: async (_, { id }, { db }) => {
      const { type, id: rawId } = fromGlobalId(id);

      switch (type) {
        case 'User':
          return db.users.findById(rawId);
        case 'Post':
          return db.posts.findById(rawId);
        default:
          return null;
      }
    },
  },

  User: {
    id: (user) => toGlobalId('User', user.id),
  },

  Post: {
    id: (post) => toGlobalId('Post', post.id),
  },
};

const httpLink = new HttpLink({ uri: 'http://localhost:4000/graphql', });

Server Implementations

WebSocket链接用于订阅

Apollo Server (TypeScript)

typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  plugins: [
    // Custom plugin
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response }) {
            console.log('Response:', response);
          },
        };
      },
    },
  ],
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => ({
    token: req.headers.authorization,
    db: database,
  }),
});
const wsLink = new GraphQLWsLink( createClient({ url: 'ws://localhost:4000/graphql', }) );

GraphQL Yoga (Modern Alternative)

根据操作类型拆分链接

typescript
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';

const yoga = createYoga({
  schema: createSchema({
    typeDefs,
    resolvers,
  }),
  graphiql: true,
  context: ({ request }) => ({
    userId: request.headers.get('x-user-id'),
  }),
});

const server = createServer(yoga);
server.listen(4000, () => {
  console.log('Server on http://localhost:4000/graphql');
});
const splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return ( definition.kind === 'OperationDefinition' && definition.operation === 'subscription' ); }, wsLink, httpLink );
const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), });

Graphene (Python/Django)

使用订阅

python
import graphene
from graphene_django import DjangoObjectType
from .models import User, Post

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = '__all__'

class PostType(DjangoObjectType):
    class Meta:
        model = Post
        fields = '__all__'

class Query(graphene.ObjectType):
    users = graphene.List(UserType)
    user = graphene.Field(UserType, id=graphene.ID(required=True))
    posts = graphene.List(PostType)

    def resolve_users(self, info):
        return User.objects.all()

    def resolve_user(self, info, id):
        return User.objects.get(pk=id)

    def resolve_posts(self, info):
        return Post.objects.all()

class CreateUser(graphene.Mutation):
    class Arguments:
        name = graphene.String(required=True)
        email = graphene.String(required=True)

    user = graphene.Field(UserType)
    success = graphene.Boolean()

    def mutate(self, info, name, email):
        user = User.objects.create(name=name, email=email)
        return CreateUser(user=user, success=True)

class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)
const MESSAGES_SUBSCRIPTION = gql
  subscription OnMessageAdded($channelId: ID!) {     messageAdded(channelId: $channelId) {       id       text       author {         name       }       createdAt     }   }
;
function ChatComponent({ channelId }) { const { data, loading } = useSubscription(MESSAGES_SUBSCRIPTION, { variables: { channelId }, });
if (loading) return <p>Loading...</p>;
return <div>New message: {data.messageAdded.text}</div>; }
undefined

Strawberry (Python, Modern)

Redis PubSub(生产环境)

python
import strawberry
from typing import List, Optional
from datetime import datetime

@strawberry.type
class User:
    id: strawberry.ID
    name: str
    email: str
    created_at: datetime

@strawberry.type
class Post:
    id: strawberry.ID
    title: str
    content: str
    author_id: strawberry.ID

@strawberry.input
class CreateUserInput:
    name: str
    email: str

@strawberry.type
class Query:
    @strawberry.field
    def users(self) -> List[User]:
        return User.objects.all()

    @strawberry.field
    def user(self, id: strawberry.ID) -> Optional[User]:
        return User.objects.get(pk=id)

@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_user(self, input: CreateUserInput) -> User:
        return User.objects.create(**input.__dict__)

schema = strawberry.Schema(query=Query, mutation=Mutation)
typescript
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: 'localhost',
  port: 6379,
  retryStrategy: (times) => Math.min(times * 50, 2000),
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

FastAPI integration

使用方式与内存PubSub相同

from strawberry.fastapi import GraphQLRouter
app = FastAPI() app.include_router(GraphQLRouter(schema), prefix="/graphql")

---
pubsub.publish('POST_CREATED', { postCreated: post }); pubsub.asyncIterator(['POST_CREATED']);

---

Client Integrations

错误处理

Apollo Client (React)

错误类型

typescript
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <UserList />
    </ApolloProvider>
  );
}

function UserList() {
  const { loading, error, data, refetch } = useQuery(GET_USERS);
  const [createUser] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      <button onClick={() => createUser({ variables: { name: 'New User' } })}>
        Add User
      </button>
    </div>
  );
}
typescript
import { GraphQLError } from 'graphql';

urql (Lightweight Alternative)

自定义错误类

typescript
import { createClient, Provider, useQuery, useMutation } from 'urql';

const client = createClient({
  url: 'http://localhost:4000/graphql',
});

function App() {
  return (
    <Provider value={client}>
      <UserList />
    </Provider>
  );
}

function UserList() {
  const [result, reexecuteQuery] = useQuery({ query: GET_USERS });
  const [, createUser] = useMutation(CREATE_USER);

  const { data, fetching, error } = result;

  if (fetching) return <p>Loading...</p>;
  if (error) return <p>Error: {error.message}</p>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}
class AuthenticationError extends GraphQLError { constructor(message: string) { super(message, { extensions: { code: 'UNAUTHENTICATED', http: { status: 401 }, }, }); } }
class ForbiddenError extends GraphQLError { constructor(message: string) { super(message, { extensions: { code: 'FORBIDDEN', http: { status: 403 }, }, }); } }
class ValidationError extends GraphQLError { constructor(message: string, invalidFields: Record<string, string>) { super(message, { extensions: { code: 'BAD_USER_INPUT', invalidFields, }, }); } }
undefined

graphql-request (Minimal)

在解析器中抛出错误

typescript
import { GraphQLClient, gql } from 'graphql-request';

const client = new GraphQLClient('http://localhost:4000/graphql');

async function fetchUsers() {
  const query = gql`
    query {
      users {
        id
        name
      }
    }
  `;

  const data = await client.request(query);
  return data.users;
}

async function createUser(name: string) {
  const mutation = gql`
    mutation CreateUser($name: String!) {
      createUser(name: $name) {
        id
        name
      }
    }
  `;

  const data = await client.request(mutation, { name });
  return data.createUser;
}
typescript
const resolvers = {
  Query: {
    user: async (_, { id }, { db, userId }) => {
      if (!userId) {
        throw new AuthenticationError('必须登录');
      }

      const user = await db.users.findById(id);
      if (!user) {
        throw new GraphQLError('用户不存在', {
          extensions: { code: 'NOT_FOUND' },
        });
      }

      return user;
    },
  },

  Mutation: {
    createPost: async (_, { input }, { db, userId }) => {
      const errors: Record<string, string> = {};

      if (!input.title || input.title.length < 3) {
        errors.title = '标题长度至少为3个字符';
      }

      if (!input.content) {
        errors.content = '内容不能为空';
      }

      if (Object.keys(errors).length > 0) {
        throw new ValidationError('输入无效', errors);
      }

      const post = await db.posts.create({ ...input, authorId: userId });
      return { post, success: true, userErrors: [] };
    },
  },
};

TanStack Query + GraphQL

错误响应格式

typescript
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GraphQLClient } from 'graphql-request';

const graphQLClient = new GraphQLClient('http://localhost:4000/graphql');

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const { users } = await graphQLClient.request(GET_USERS);
      return users;
    },
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (name: string) => {
      const { createUser } = await graphQLClient.request(CREATE_USER, { name });
      return createUser;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

json
{
  "errors": [
    {
      "message": "用户不存在",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND"
      }
    }
  ],
  "data": {
    "user": null
  }
}

TypeScript Code Generation

字段级错误处理

GraphQL Code Generator Setup

bash
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo
typescript
undefined

codegen.yml

可空字段允许返回部分结果

yaml
schema: http://localhost:4000/graphql
documents: 'src/**/*.graphql'
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withComponent: false
      skipTypename: false
      enumsAsTypes: true
type Query { user(id: ID!): User # 出错时返回null users: [User!]! # 出错时抛出异常 post(id: ID!): Post # 出错时返回null }

Generated Types

解析器可以返回null而不是抛出异常

typescript
// src/queries/users.graphql
// query GetUsers {
//   users {
//     id
//     name
//     email
//   }
// }

// Generated types
export type GetUsersQuery = {
  __typename?: 'Query';
  users: Array<{
    __typename?: 'User';
    id: string;
    name: string;
    email: string;
  }>;
};

export function useGetUsersQuery(
  baseOptions?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>
) {
  return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(
    GetUsersDocument,
    baseOptions
  );
}
const resolvers = { Query: { user: async (_, { id }, { db }) => { try { return await db.users.findById(id); } catch (error) { console.error('获取用户失败:', error); return null; # 返回null而不是错误 } }, }, };

---

Usage with Generated Types

Schema设计模式

Relay游标连接

typescript
import { useGetUsersQuery, useCreateUserMutation } from './generated/graphql';

function UserList() {
  const { data, loading, error } = useGetUsersQuery();
  const [createUser] = useCreateUserMutation();

  // Fully typed!
  const users = data?.users; // Type: User[] | undefined
}

graphql
type PageInfo {
  hasNextPage: Boolean!
  hasPreviousPage: Boolean!
  startCursor: String
  endCursor: String
}

type PostEdge {
  node: Post!
  cursor: String!
}

type PostConnection {
  edges: [PostEdge!]!
  pageInfo: PageInfo!
  totalCount: Int!
}

type Query {
  posts(
    first: Int
    after: String
    last: Int
    before: String
  ): PostConnection!
}

Authentication and Authorization

Relay连接解析器

Context-Based Auth

typescript
import jwt from 'jsonwebtoken';

async function createContext({ req }) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return { userId: null, db };
  }

  try {
    const { userId } = jwt.verify(token, process.env.JWT_SECRET);
    return { userId, db };
  } catch (error) {
    return { userId: null, db };
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

await startStandaloneServer(server, {
  context: createContext,
});
typescript
import { fromGlobalId, toGlobalId } from 'graphql-relay';

function encodeCursor(id: string): string {
  return Buffer.from(`cursor:${id}`).toString('base64');
}

function decodeCursor(cursor: string): string {
  return Buffer.from(cursor, 'base64').toString('utf-8').replace('cursor:', '');
}

const resolvers = {
  Query: {
    posts: async (_, { first = 10, after }, { db }) => {
      const startId = after ? decodeCursor(after) : null;

      # 获取first + 1条数据以判断是否有下一页
      const posts = await db.posts.findMany({
        where: startId ? { id: { gt: startId } } : {},
        take: first + 1,
        orderBy: { createdAt: 'desc' },
      });

      const hasNextPage = posts.length > first;
      const nodes = hasNextPage ? posts.slice(0, -1) : posts;

      const edges = nodes.map(node => ({
        node,
        cursor: encodeCursor(node.id),
      }));

      return {
        edges,
        pageInfo: {
          hasNextPage,
          hasPreviousPage: !!after,
          startCursor: edges[0]?.cursor,
          endCursor: edges[edges.length - 1]?.cursor,
        },
        totalCount: await db.posts.count(),
      };
    },
  },
};

Resolver-Level Auth

偏移分页(更简单)

typescript
const resolvers = {
  Query: {
    me: (_, __, { userId }) => {
      if (!userId) {
        throw new AuthenticationError('Not authenticated');
      }
      return db.users.findById(userId);
    },

    users: (_, __, { userId, db }) => {
      if (!userId) {
        throw new AuthenticationError('Not authenticated');
      }

      const user = db.users.findById(userId);
      if (user.role !== 'ADMIN') {
        throw new ForbiddenError('Admin access required');
      }

      return db.users.findMany();
    },
  },
};
graphql
type PostsResponse {
  posts: [Post!]!
  total: Int!
  hasMore: Boolean!
}

type Query {
  posts(limit: Int = 10, offset: Int = 0): PostsResponse!
}

Directive-Based Auth

全局对象标识(Relay)

typescript
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

// Schema with directive
const typeDefs = gql`
  directive @auth(requires: Role = USER) on FIELD_DEFINITION

  enum Role {
    ADMIN
    USER
  }

  type Query {
    users: [User!]! @auth(requires: ADMIN)
    me: User! @auth
  }
`;

// Directive transformer
function authDirectiveTransformer(schema, directiveName = 'auth') {
  return mapSchema(schema, {
    [MapperKind.OBJECT_FIELD]: (fieldConfig) => {
      const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];

      if (authDirective) {
        const { requires } = authDirective;
        const { resolve = defaultFieldResolver } = fieldConfig;

        fieldConfig.resolve = async (source, args, context, info) => {
          if (!context.userId) {
            throw new AuthenticationError('Not authenticated');
          }

          if (requires) {
            const user = await context.db.users.findById(context.userId);
            if (user.role !== requires) {
              throw new ForbiddenError(`${requires} role required`);
            }
          }

          return resolve(source, args, context, info);
        };
      }

      return fieldConfig;
    },
  });
}

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = authDirectiveTransformer(schema);

graphql
interface Node {
  id: ID!  # 全局唯一ID
}

type User implements Node {
  id: ID!
  name: String!
}

type Post implements Node {
  id: ID!
  title: String!
}

type Query {
  node(id: ID!): Node
}
typescript
const resolvers = {
  Query: {
    node: async (_, { id }, { db }) => {
      const { type, id: rawId } = fromGlobalId(id);

      switch (type) {
        case 'User':
          return db.users.findById(rawId);
        case 'Post':
          return db.posts.findById(rawId);
        default:
          return null;
      }
    },
  },

  User: {
    id: (user) => toGlobalId('User', user.id),
  },

  Post: {
    id: (post) => toGlobalId('Post', post.id),
  },
};

Performance Optimization

服务器实现

Query Complexity Analysis

Apollo Server(TypeScript)

typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
    }),
  ],
});
typescript
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  introspection: process.env.NODE_ENV !== 'production',
  plugins: [
    # 自定义插件
    {
      async requestDidStart() {
        return {
          async willSendResponse({ response }) {
            console.log('Response:', response);
          },
        };
      },
    },
  ],
});

const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
  context: async ({ req }) => ({
    token: req.headers.authorization,
    db: database,
  }),
});

Persistent Queries (APQ)

GraphQL Yoga(现代替代方案)

typescript
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new Map(), // Or Redis
  },
});

// Client sends hash instead of full query
// Reduces payload size by ~80%
typescript
import { createYoga, createSchema } from 'graphql-yoga';
import { createServer } from 'node:http';

const yoga = createYoga({
  schema: createSchema({
    typeDefs,
    resolvers,
  }),
  graphiql: true,
  context: ({ request }) => ({
    userId: request.headers.get('x-user-id'),
  }),
});

const server = createServer(yoga);
server.listen(4000, () => {
  console.log('Server on http://localhost:4000/graphql');
});

Response Caching

Graphene(Python/Django)

typescript
import responseCachePlugin from '@apollo/server-plugin-response-cache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    responseCachePlugin({
      sessionId: (context) => context.userId || null,
      shouldReadFromCache: (context) => !context.userId, // Cache only public queries
    }),
  ],
});

// Schema directive for cache control
const typeDefs = gql`
  type Query {
    posts: [Post!]! @cacheControl(maxAge: 60)
    user(id: ID!): User @cacheControl(maxAge: 30)
  }
`;
python
import graphene
from graphene_django import DjangoObjectType
from .models import User, Post

class UserType(DjangoObjectType):
    class Meta:
        model = User
        fields = '__all__'

class PostType(DjangoObjectType):
    class Meta:
        model = Post
        fields = '__all__'

class Query(graphene.ObjectType):
    users = graphene.List(UserType)
    user = graphene.Field(UserType, id=graphene.ID(required=True))
    posts = graphene.List(PostType)

    def resolve_users(self, info):
        return User.objects.all()

    def resolve_user(self, info, id):
        return User.objects.get(pk=id)

    def resolve_posts(self, info):
        return Post.objects.all()

class CreateUser(graphene.Mutation):
    class Arguments:
        name = graphene.String(required=True)
        email = graphene.String(required=True)

    user = graphene.Field(UserType)
    success = graphene.Boolean()

    def mutate(self, info, name, email):
        user = User.objects.create(name=name, email=email)
        return CreateUser(user=user, success=True)

class Mutation(graphene.ObjectType):
    create_user = CreateUser.Field()

schema = graphene.Schema(query=Query, mutation=Mutation)

Field-Level Caching

Strawberry(Python,现代)

typescript
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const cache = new InMemoryLRUCache({
  maxSize: Math.pow(2, 20) * 100, // 100 MB
  ttl: 300, // 5 minutes
});

const resolvers = {
  Query: {
    user: async (_, { id }, { cache, db }) => {
      const cacheKey = `user:${id}`;
      const cached = await cache.get(cacheKey);

      if (cached) {
        return JSON.parse(cached);
      }

      const user = await db.users.findById(id);
      await cache.set(cacheKey, JSON.stringify(user), { ttl: 60 });

      return user;
    },
  },
};

python
import strawberry
from typing import List, Optional
from datetime import datetime

@strawberry.type
class User:
    id: strawberry.ID
    name: str
    email: str
    created_at: datetime

@strawberry.type
class Post:
    id: strawberry.ID
    title: str
    content: str
    author_id: strawberry.ID

@strawberry.input
class CreateUserInput:
    name: str
    email: str

@strawberry.type
class Query:
    @strawberry.field
    def users(self) -> List[User]:
        return User.objects.all()

    @strawberry.field
    def user(self, id: strawberry.ID) -> Optional[User]:
        return User.objects.get(pk=id)

@strawberry.type
class Mutation:
    @strawberry.mutation
    def create_user(self, input: CreateUserInput) -> User:
        return User.objects.create(**input.__dict__)

schema = strawberry.Schema(query=Query, mutation=Mutation)

File Uploads

FastAPI集成

Schema

graphql
scalar Upload

type Mutation {
  uploadFile(file: Upload!): File!
  uploadMultiple(files: [Upload!]!): [File!]!
}

type File {
  filename: String!
  mimetype: String!
  encoding: String!
  url: String!
}
from strawberry.fastapi import GraphQLRouter
app = FastAPI() app.include_router(GraphQLRouter(schema), prefix="/graphql")

---

Server (graphql-upload)

客户端集成

Apollo Client(React)

typescript
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { GraphQLUpload } from 'graphql-upload/GraphQLUpload.mjs';
import fs from 'fs';
import path from 'path';

app.use('/graphql', graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));

const resolvers = {
  Upload: GraphQLUpload,

  Mutation: {
    uploadFile: async (_, { file }) => {
      const { createReadStream, filename, mimetype, encoding } = await file;

      const stream = createReadStream();
      const uploadPath = path.join(__dirname, 'uploads', filename);

      await new Promise((resolve, reject) => {
        stream
          .pipe(fs.createWriteStream(uploadPath))
          .on('finish', resolve)
          .on('error', reject);
      });

      return {
        filename,
        mimetype,
        encoding,
        url: `/uploads/${filename}`,
      };
    },
  },
};
typescript
import { ApolloClient, InMemoryCache, ApolloProvider, useQuery, useMutation } from '@apollo/client';

const client = new ApolloClient({
  uri: 'http://localhost:4000/graphql',
  cache: new InMemoryCache(),
});

function App() {
  return (
    <ApolloProvider client={client}>
      <UserList />
    </ApolloProvider>
  );
}

function UserList() {
  const { loading, error, data, refetch } = useQuery(GET_USERS);
  const [createUser] = useMutation(CREATE_USER, {
    refetchQueries: [{ query: GET_USERS }],
  });

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
      <button onClick={() => createUser({ variables: { name: '新用户' } })}>
        添加用户
      </button>
    </div>
  );
}

Client (Apollo Client)

urql(轻量替代方案)

typescript
import { useMutation } from '@apollo/client';

const UPLOAD_FILE = gql`
  mutation UploadFile($file: Upload!) {
    uploadFile(file: $file) {
      filename
      url
    }
  }
`;

function FileUpload() {
  const [uploadFile] = useMutation(UPLOAD_FILE);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    uploadFile({ variables: { file } });
  };

  return <input type="file" onChange={handleFileChange} />;
}

typescript
import { createClient, Provider, useQuery, useMutation } from 'urql';

const client = createClient({
  url: 'http://localhost:4000/graphql',
});

function App() {
  return (
    <Provider value={client}>
      <UserList />
    </Provider>
  );
}

function UserList() {
  const [result, reexecuteQuery] = useQuery({ query: GET_USERS });
  const [, createUser] = useMutation(CREATE_USER);

  const { data, fetching, error } = result;

  if (fetching) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

Testing

graphql-request(极简)

Unit Testing Resolvers

typescript
import { describe, it, expect, vi } from 'vitest';

describe('User Resolvers', () => {
  it('should fetch user by ID', async () => {
    const mockDb = {
      users: {
        findById: vi.fn().mockResolvedValue({
          id: '1',
          name: 'Alice',
          email: 'alice@example.com',
        }),
      },
    };

    const result = await resolvers.Query.user(
      null,
      { id: '1' },
      { db: mockDb, userId: '1' },
      {} as any
    );

    expect(result.name).toBe('Alice');
    expect(mockDb.users.findById).toHaveBeenCalledWith('1');
  });

  it('should throw error when not authenticated', async () => {
    await expect(
      resolvers.Query.me(null, {}, { userId: null }, {} as any)
    ).rejects.toThrow('Not authenticated');
  });
});
typescript
import { GraphQLClient, gql } from 'graphql-request';

const client = new GraphQLClient('http://localhost:4000/graphql');

async function fetchUsers() {
  const query = gql`
    query {
      users {
        id
        name
      }
    }
  `;

  const data = await client.request(query);
  return data.users;
}

async function createUser(name: string) {
  const mutation = gql`
    mutation CreateUser($name: String!) {
      createUser(name: $name) {
        id
        name
      }
    }
  `;

  const data = await client.request(mutation, { name });
  return data.createUser;
}

Integration Testing (Apollo Server)

TanStack Query + GraphQL

typescript
import { ApolloServer } from '@apollo/server';
import assert from 'assert';

it('fetches users', async () => {
  const server = new ApolloServer({ typeDefs, resolvers });

  const response = await server.executeOperation({
    query: 'query { users { id name } }',
  });

  assert(response.body.kind === 'single');
  expect(response.body.singleResult.errors).toBeUndefined();
  expect(response.body.singleResult.data?.users).toHaveLength(2);
});

it('creates user', async () => {
  const response = await server.executeOperation({
    query: `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          user { id name }
          success
        }
      }
    `,
    variables: {
      input: { name: 'Charlie', email: 'charlie@example.com' },
    },
  });

  assert(response.body.kind === 'single');
  expect(response.body.singleResult.data?.createUser.success).toBe(true);
});
typescript
import { useQuery, useMutation, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { GraphQLClient } from 'graphql-request';

const graphQLClient = new GraphQLClient('http://localhost:4000/graphql');

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const { users } = await graphQLClient.request(GET_USERS);
      return users;
    },
  });
}

function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (name: string) => {
      const { createUser } = await graphQLClient.request(CREATE_USER, { name });
      return createUser;
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] });
    },
  });
}

E2E Testing (Supertest)

TypeScript代码生成

GraphQL Code Generator配置

typescript
import request from 'supertest';
import { app } from './server';

describe('GraphQL API', () => {
  it('should query users', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({
        query: '{ users { id name } }',
      })
      .expect(200);

    expect(response.body.data.users).toBeDefined();
  });

  it('should require authentication', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({
        query: '{ me { id } }',
      })
      .expect(200);

    expect(response.body.errors[0].extensions.code).toBe('UNAUTHENTICATED');
  });
});

bash
npm install -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-react-apollo

Production Patterns

codegen.yml

Schema Stitching

typescript
import { stitchSchemas } from '@graphql-tools/stitch';

const userSchema = makeExecutableSchema({ typeDefs: userTypeDefs, resolvers: userResolvers });
const postSchema = makeExecutableSchema({ typeDefs: postTypeDefs, resolvers: postResolvers });

const schema = stitchSchemas({
  subschemas: [
    { schema: userSchema },
    { schema: postSchema },
  ],
});
yaml
schema: http://localhost:4000/graphql
documents: 'src/**/*.graphql'
generates:
  src/generated/graphql.ts:
    plugins:
      - typescript
      - typescript-operations
      - typescript-react-apollo
    config:
      withHooks: true
      withComponent: false
      skipTypename: false
      enumsAsTypes: true

Apollo Federation

生成的类型

typescript
// User service
import { buildSubgraphSchema } from '@apollo/subgraph';

const typeDefs = gql`
  type User @key(fields: "id") {
    id: ID!
    name: String!
    email: String!
  }
`;

const resolvers = {
  User: {
    __resolveReference(user, { db }) {
      return db.users.findById(user.id);
    },
  },
};

const schema = buildSubgraphSchema({ typeDefs, resolvers });

// Post service
const typeDefs = gql`
  extend type User @key(fields: "id") {
    id: ID! @external
    posts: [Post!]!
  }

  type Post @key(fields: "id") {
    id: ID!
    title: String!
    authorId: ID!
  }
`;

// Gateway
import { ApolloGateway } from '@apollo/gateway';

const gateway = new ApolloGateway({
  supergraphSdl: readFileSync('./supergraph.graphql', 'utf-8'),
});

const server = new ApolloServer({ gateway });
typescript
// src/queries/users.graphql
// query GetUsers {
//   users {
//     id
//     name
//     email
//   }
// }

// 生成的类型
export type GetUsersQuery = {
  __typename?: 'Query';
  users: Array<{
    __typename?: 'User';
    id: string;
    name: string;
    email: string;
  }>;
};

export function useGetUsersQuery(
  baseOptions?: Apollo.QueryHookOptions<GetUsersQuery, GetUsersQueryVariables>
) {
  return Apollo.useQuery<GetUsersQuery, GetUsersQueryVariables>(
    GetUsersDocument,
    baseOptions
  );
}

Rate Limiting

使用生成的类型

typescript
import { GraphQLRateLimitDirective } from 'graphql-rate-limit-directive';

const typeDefs = gql`
  directive @rateLimit(
    limit: Int = 10
    duration: Int = 60
  ) on FIELD_DEFINITION

  type Query {
    users: [User!]! @rateLimit(limit: 100, duration: 60)
    search(query: String!): [Result!]! @rateLimit(limit: 10, duration: 60)
  }
`;

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = GraphQLRateLimitDirective()(schema);
typescript
import { useGetUsersQuery, useCreateUserMutation } from './generated/graphql';

function UserList() {
  const { data, loading, error } = useGetUsersQuery();
  const [createUser] = useCreateUserMutation();

  // 完全类型化!
  const users = data?.users; // 类型: User[] | undefined
}

Monitoring (Apollo Studio)

认证与授权

基于上下文的认证

typescript
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginUsageReporting({
      sendVariableValues: { all: true },
      sendHeaders: { all: true },
    }),
  ],
});

typescript
import jwt from 'jsonwebtoken';

async function createContext({ req }) {
  const token = req.headers.authorization?.replace('Bearer ', '');

  if (!token) {
    return { userId: null, db };
  }

  try {
    const { userId } = jwt.verify(token, process.env.JWT_SECRET);
    return { userId, db };
  } catch (error) {
    return { userId: null, db };
  }
}

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

await startStandaloneServer(server, {
  context: createContext,
});

Framework Integration

解析器级别的授权

Next.js App Router

typescript
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const handler = startServerAndCreateNextHandler(server);

export { handler as GET, handler as POST };
typescript
const resolvers = {
  Query: {
    me: (_, __, { userId }) => {
      if (!userId) {
        throw new AuthenticationError('未认证');
      }
      return db.users.findById(userId);
    },

    users: (_, __, { userId, db }) => {
      if (!userId) {
        throw new AuthenticationError('未认证');
      }

      const user = db.users.findById(userId);
      if (user.role !== 'ADMIN') {
        throw new ForbiddenError('需要管理员权限');
      }

      return db.users.findMany();
    },
  },
};

Next.js with Apollo Client (SSR)

基于指令的授权

typescript
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';

export const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      uri: 'http://localhost:4000/graphql',
    }),
  });
});

// app/users/page.tsx
import { getClient } from '@/lib/apollo-client';
import { gql } from '@apollo/client';

export default async function UsersPage() {
  const { data } = await getClient().query({
    query: gql`
      query GetUsers {
        users {
          id
          name
        }
      }
    `,
  });

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}
typescript
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';

FastAPI + Strawberry

带指令的Schema

python
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

app = FastAPI()

graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

const typeDefs = gql` directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role { ADMIN USER }
type Query { users: [User!]! @auth(requires: ADMIN) me: User! @auth } `;

Comparison with REST and tRPC

指令转换器

GraphQL vs REST

FeatureGraphQLREST
EndpointsSingle endpointMultiple endpoints
Data FetchingClient specifies fieldsServer determines response
Over-fetchingNoYes (extra fields)
Under-fetchingNoYes (multiple requests)
VersioningNot neededRequired (v1, v2)
CachingComplexSimple (HTTP caching)
Type SystemBuilt-inExternal (OpenAPI)
Real-timeSubscriptionsSSE/WebSocket
function authDirectiveTransformer(schema, directiveName = 'auth') { return mapSchema(schema, { [MapperKind.OBJECT_FIELD]: (fieldConfig) => { const authDirective = getDirective(schema, fieldConfig, directiveName)?.[0];
  if (authDirective) {
    const { requires } = authDirective;
    const { resolve = defaultFieldResolver } = fieldConfig;

    fieldConfig.resolve = async (source, args, context, info) => {
      if (!context.userId) {
        throw new AuthenticationError('未认证');
      }

      if (requires) {
        const user = await context.db.users.findById(context.userId);
        if (user.role !== requires) {
          throw new ForbiddenError(`需要${requires}角色`);
        }
      }

      return resolve(source, args, context, info);
    };
  }

  return fieldConfig;
},
}); }
let schema = makeExecutableSchema({ typeDefs, resolvers }); schema = authDirectiveTransformer(schema);

---

GraphQL vs tRPC

性能优化

查询复杂度分析

FeatureGraphQLtRPC
Type SafetyCodegen requiredNative TypeScript
Language SupportAnyTypeScript only
Client-Server CouplingLooseTight
SchemaSDL requiredInferred from code
Learning CurveSteepGentle
ToolingExtensiveGrowing
Use CasePublic APIs, MobileFull-stack TypeScript

typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [
    createComplexityLimitRule(1000, {
      scalarCost: 1,
      objectCost: 2,
      listFactor: 10,
    }),
  ],
});

Migration Strategies

持久化查询(APQ)

REST to GraphQL (Gradual)

typescript
// 1. Wrap existing REST endpoints
const resolvers = {
  Query: {
    user: async (_, { id }) => {
      const response = await fetch(`https://api.example.com/users/${id}`);
      return response.json();
    },
  },
};

// 2. Add GraphQL layer alongside REST
app.use('/api/rest', restRouter);
app.use('/graphql', graphqlMiddleware);

// 3. Migrate clients incrementally
// 4. Deprecate REST endpoints when ready
typescript
import { ApolloServer } from '@apollo/server';
import { ApolloServerPluginInlineTraceDisabled } from '@apollo/server/plugin/disabled';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new Map(), // 或使用Redis
  },
});

Adding GraphQL to Existing App

客户端发送哈希而不是完整查询

有效负载大小减少约80%

typescript
// Express + GraphQL
import express from 'express';
import { expressMiddleware } from '@apollo/server/express4';

const app = express();

// Existing routes
app.use('/api', existingApiRouter);

// Add GraphQL
app.use('/graphql', express.json(), expressMiddleware(server));

undefined

Best Practices

响应缓存

Schema Design

  • Use semantic field names (
    createdAt
    , not
    created_at
    )
  • Prefer specific types over generic JSON
  • Use enums for fixed value sets
  • Design for client use cases, not database structure
  • Use input types for complex mutations
  • Implement pagination for lists
  • Follow Relay specification for connections
typescript
import responseCachePlugin from '@apollo/server-plugin-response-cache';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    responseCachePlugin({
      sessionId: (context) => context.userId || null,
      shouldReadFromCache: (context) => !context.userId, // 仅缓存公共查询
    }),
  ],
});

Resolver Patterns

用于缓存控制的Schema指令

  • Keep resolvers thin, delegate to service layer
  • Use DataLoader for all database fetches
  • Validate inputs in resolvers, not database layer
  • Return errors in payload, not just exceptions
  • Use context for shared dependencies (db, auth, loaders)
const typeDefs = gql
  type Query {     posts: [Post!]! @cacheControl(maxAge: 60)     user(id: ID!): User @cacheControl(maxAge: 30)   }
;
undefined

Error Handling

字段级缓存

  • Use custom error types with error codes
  • Return field-level errors for mutations
  • Log errors server-side, sanitize for clients
  • Use nullable fields to allow partial results
  • Don't expose internal implementation details
typescript
import { InMemoryLRUCache } from '@apollo/utils.keyvaluecache';

const cache = new InMemoryLRUCache({
  maxSize: Math.pow(2, 20) * 100, // 100 MB
  ttl: 300, // 5分钟
});

const resolvers = {
  Query: {
    user: async (_, { id }, { cache, db }) => {
      const cacheKey = `user:${id}`;
      const cached = await cache.get(cacheKey);

      if (cached) {
        return JSON.parse(cached);
      }

      const user = await db.users.findById(id);
      await cache.set(cacheKey, JSON.stringify(user), { ttl: 60 });

      return user;
    },
  },
};

Performance

文件上传

Schema

  • Always use DataLoader to prevent N+1
  • Implement query complexity limits
  • Cache frequently accessed data
  • Use persisted queries in production
  • Monitor slow queries and optimize
  • Batch mutations when possible
graphql
scalar Upload

type Mutation {
  uploadFile(file: Upload!): File!
  uploadMultiple(files: [Upload!]!): [File!]!
}

type File {
  filename: String!
  mimetype: String!
  encoding: String!
  url: String!
}

Security

服务器(graphql-upload)

  • Implement authentication and authorization
  • Validate all inputs
  • Use query depth limiting
  • Implement rate limiting per user
  • Disable introspection in production (optional)
  • Sanitize error messages
typescript
import graphqlUploadExpress from 'graphql-upload/graphqlUploadExpress.mjs';
import { GraphQLUpload } from 'graphql-upload/GraphQLUpload.mjs';
import fs from 'fs';
import path from 'path';

app.use('/graphql', graphqlUploadExpress({ maxFileSize: 10000000, maxFiles: 10 }));

const resolvers = {
  Upload: GraphQLUpload,

  Mutation: {
    uploadFile: async (_, { file }) => {
      const { createReadStream, filename, mimetype, encoding } = await file;

      const stream = createReadStream();
      const uploadPath = path.join(__dirname, 'uploads', filename);

      await new Promise((resolve, reject) => {
        stream
          .pipe(fs.createWriteStream(uploadPath))
          .on('finish', resolve)
          .on('error', reject);
      });

      return {
        filename,
        mimetype,
        encoding,
        url: `/uploads/${filename}`,
      };
    },
  },
};

Testing

客户端(Apollo Client)

  • Test resolvers in isolation
  • Mock external dependencies
  • Test error conditions
  • Integration test critical flows
  • E2E test with real client
typescript
import { useMutation } from '@apollo/client';

const UPLOAD_FILE = gql`
  mutation UploadFile($file: Upload!) {
    uploadFile(file: $file) {
      filename
      url
    }
  }
`;

function FileUpload() {
  const [uploadFile] = useMutation(UPLOAD_FILE);

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    uploadFile({ variables: { file } });
  };

  return <input type="file" onChange={handleFileChange} />;
}

Documentation

测试

解析器单元测试

  • Write clear field descriptions
  • Document deprecations with
    @deprecated
  • Provide usage examples in schema comments
  • Keep schema documentation up-to-date

typescript
import { describe, it, expect, vi } from 'vitest';

describe('User Resolvers', () => {
  it('should fetch user by ID', async () => {
    const mockDb = {
      users: {
        findById: vi.fn().mockResolvedValue({
          id: '1',
          name: 'Alice',
          email: 'alice@example.com',
        }),
      },
    };

    const result = await resolvers.Query.user(
      null,
      { id: '1' },
      { db: mockDb, userId: '1' },
      {} as any
    );

    expect(result.name).toBe('Alice');
    expect(mockDb.users.findById).toHaveBeenCalledWith('1');
  });

  it('should throw error when not authenticated', async () => {
    await expect(
      resolvers.Query.me(null, {}, { userId: null }, {} as any)
    ).rejects.toThrow('Not authenticated');
  });
});

Summary

集成测试(Apollo Server)

GraphQL provides a powerful, flexible API layer with strong typing, efficient data fetching, and excellent developer experience. Key advantages include:
  • Client Control: Fetch exactly what you need
  • Type Safety: Schema-first design with introspection
  • Single Endpoint: Simplified API surface
  • Real-time: Built-in subscription support
  • Tooling: Excellent ecosystem (Apollo, Relay, codegen)
Trade-offs to Consider:
  • More complex than REST for simple CRUD
  • Caching requires more thought than HTTP caching
  • Learning curve for teams new to GraphQL
  • Query complexity can impact performance
Best For:
  • Mobile apps needing bandwidth efficiency
  • Complex frontends with varied data needs
  • Microservices aggregation
  • Real-time applications
  • Multi-platform clients (web, mobile, IoT)
Start Simple:
  1. Define schema for core entities
  2. Write resolvers with DataLoader
  3. Add authentication/authorization
  4. Implement error handling
  5. Optimize with caching
  6. Add subscriptions if needed
  7. Monitor and iterate
GraphQL shines when API flexibility and developer experience are priorities. Combined with TypeScript code generation, it provides end-to-end type safety from database to UI.
typescript
import { ApolloServer } from '@apollo/server';
import assert from 'assert';

it('fetches users', async () => {
  const server = new ApolloServer({ typeDefs, resolvers });

  const response = await server.executeOperation({
    query: 'query { users { id name } }',
  });

  assert(response.body.kind === 'single');
  expect(response.body.singleResult.errors).toBeUndefined();
  expect(response.body.singleResult.data?.users).toHaveLength(2);
});

it('creates user', async () => {
  const response = await server.executeOperation({
    query: `
      mutation CreateUser($input: CreateUserInput!) {
        createUser(input: $input) {
          user { id name }
          success
        }
      }
    `,
    variables: {
      input: { name: 'Charlie', email: 'charlie@example.com' },
    },
  });

  assert(response.body.kind === 'single');
  expect(response.body.singleResult.data?.createUser.success).toBe(true);
});

E2E测试(Supertest)

typescript
import request from 'supertest';
import { app } from './server';

describe('GraphQL API', () => {
  it('should query users', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({
        query: '{ users { id name } }',
      })
      .expect(200);

    expect(response.body.data.users).toBeDefined();
  });

  it('should require authentication', async () => {
    const response = await request(app)
      .post('/graphql')
      .send({
        query: '{ me { id } }',
      })
      .expect(200);

    expect(response.body.errors[0].extensions.code).toBe('UNAUTHENTICATED');
  });
});

生产环境模式

Schema拼接

typescript
import { stitchSchemas } from '@graphql-tools/stitch';

const userSchema = makeExecutableSchema({ typeDefs: userTypeDefs, resolvers: userResolvers });
const postSchema = makeExecutableSchema({ typeDefs: postTypeDefs, resolvers: postResolvers });

const schema = stitchSchemas({
  subschemas: [
    { schema: userSchema },
    { schema: postSchema },
  ],
});

Apollo联邦

typescript
undefined

用户服务

import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql
  type User @key(fields: "id") {     id: ID!     name: String!     email: String!   }
;
const resolvers = { User: { __resolveReference(user, { db }) { return db.users.findById(user.id); }, }, };
const schema = buildSubgraphSchema({ typeDefs, resolvers });

帖子服务

const typeDefs = gql` extend type User @key(fields: "id") { id: ID! @external posts: [Post!]! }
type Post @key(fields: "id") { id: ID! title: String! authorId: ID! } `;

网关

import { ApolloGateway } from '@apollo/gateway';
const gateway = new ApolloGateway({ supergraphSdl: readFileSync('./supergraph.graphql', 'utf-8'), });
const server = new ApolloServer({ gateway });
undefined

速率限制

typescript
import { GraphQLRateLimitDirective } from 'graphql-rate-limit-directive';

const typeDefs = gql`
  directive @rateLimit(
    limit: Int = 10
    duration: Int = 60
  ) on FIELD_DEFINITION

  type Query {
    users: [User!]! @rateLimit(limit: 100, duration: 60)
    search(query: String!): [Result!]! @rateLimit(limit: 10, duration: 60)
  }
`;

let schema = makeExecutableSchema({ typeDefs, resolvers });
schema = GraphQLRateLimitDirective()(schema);

监控(Apollo Studio)

typescript
import { ApolloServerPluginUsageReporting } from '@apollo/server/plugin/usageReporting';

const server = new ApolloServer({
  typeDefs,
  resolvers,
  plugins: [
    ApolloServerPluginUsageReporting({
      sendVariableValues: { all: true },
      sendHeaders: { all: true },
    }),
  ],
});

框架集成

Next.js App Router

typescript
// app/api/graphql/route.ts
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

const handler = startServerAndCreateNextHandler(server);

export { handler as GET, handler as POST };

Next.js with Apollo Client(SSR)

typescript
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
import { registerApolloClient } from '@apollo/experimental-nextjs-app-support/rsc';

export const { getClient } = registerApolloClient(() => {
  return new ApolloClient({
    cache: new InMemoryCache(),
    link: new HttpLink({
      uri: 'http://localhost:4000/graphql',
    }),
  });
});

// app/users/page.tsx
import { getClient } from '@/lib/apollo-client';
import { gql } from '@apollo/client';

export default async function UsersPage() {
  const { data } = await getClient().query({
    query: gql`
      query GetUsers {
        users {
          id
          name
        }
      }
    `,
  });

  return (
    <div>
      {data.users.map(user => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

FastAPI + Strawberry

python
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter

app = FastAPI()

graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix="/graphql")

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

与REST和tRPC的对比

GraphQL vs REST

特性GraphQLREST
端点单个端点多个端点
数据获取客户端指定字段服务器决定响应内容
过度获取有(返回额外字段)
获取不足有(需要多次请求)
版本管理不需要必须(v1、v2)
缓存复杂简单(HTTP缓存)
类型系统内置外部(OpenAPI)
实时能力订阅SSE/WebSocket

GraphQL vs tRPC

特性GraphQLtRPC
类型安全需要代码生成原生TypeScript支持
语言支持任意语言仅TypeScript
客户端-服务端耦合松散紧密
Schema需要SDL从代码中推断
学习曲线陡峭平缓
工具链丰富正在发展
适用场景公共API、移动应用全栈TypeScript项目

迁移策略

REST到GraphQL(渐进式)

typescript
undefined

1. 包装现有REST端点

const resolvers = { Query: { user: async (_, { id }) => { const response = await fetch(
https://api.example.com/users/${id}
); return response.json(); }, }, };

2. 在REST旁添加GraphQL层

app.use('/api/rest', restRouter); app.use('/graphql', graphqlMiddleware);

3. 逐步迁移客户端

4. 准备就绪后弃用REST端点

undefined

为现有应用添加GraphQL

typescript
undefined

Express + GraphQL

import express from 'express'; import { expressMiddleware } from '@apollo/server/express4';
const app = express();

现有路由

app.use('/api', existingApiRouter);

添加GraphQL

app.use('/graphql', express.json(), expressMiddleware(server));

---

最佳实践

Schema设计

  • 使用语义化字段名(
    createdAt
    ,而非
    created_at
  • 优先使用特定类型而非通用JSON
  • 对固定值集合使用枚举类型
  • 针对客户端使用场景设计,而非数据库结构
  • 对复杂变更使用输入类型
  • 为列表实现分页
  • 遵循Relay规范实现连接

解析器模式

  • 保持解析器简洁,将逻辑委托给服务层
  • 对所有数据库查询使用DataLoader
  • 在解析器中验证输入,而非数据库层
  • 在负载中返回错误,而非仅抛出异常
  • 使用上下文存储共享依赖(数据库、认证、加载器)

错误处理

  • 使用带错误码的自定义错误类型
  • 为变更返回字段级错误
  • 在服务器端记录错误,对客户端返回 sanitized 后的错误
  • 使用可空字段允许返回部分结果
  • 不要暴露内部实现细节

性能

  • 始终使用DataLoader避免N+1问题
  • 实现查询复杂度限制
  • 缓存频繁访问的数据
  • 在生产环境使用持久化查询
  • 监控慢查询并优化
  • 尽可能批量处理变更

安全

  • 实现认证与授权
  • 验证所有输入
  • 使用查询深度限制
  • 按用户实现速率限制
  • 在生产环境禁用自省(可选)
  • Sanitize错误消息

测试

  • 孤立测试解析器
  • 模拟外部依赖
  • 测试错误场景
  • 集成测试关键流程
  • 使用真实客户端进行E2E测试

文档

  • 编写清晰的字段描述
  • 使用
    @deprecated
    标记废弃内容
  • 在Schema注释中提供使用示例
  • 保持Schema文档更新

总结

GraphQL提供了强大、灵活的API层,具备强类型、高效数据获取和优秀的开发者体验。主要优势包括:
  • 客户端控制:精准获取所需数据
  • 类型安全:Schema优先设计,支持自省
  • 单端点:简化API表面
  • 实时能力:内置订阅支持
  • 工具链:完善的生态系统(Apollo、Relay、代码生成)
需要考虑的权衡
  • 对于简单CRUD操作,比REST更复杂
  • 缓存比HTTP缓存需要更多思考
  • 对不熟悉GraphQL的团队有学习曲线
  • 查询复杂度可能影响性能
最佳适用场景
  • 需要带宽效率的移动应用
  • 数据需求多样的复杂前端
  • 微服务聚合
  • 实时应用
  • 多平台客户端(Web、移动、IoT)
入门建议
  1. 为核心实体定义Schema
  2. 使用DataLoader编写解析器
  3. 添加认证与授权
  4. 实现错误处理
  5. 通过缓存优化性能
  6. 按需添加订阅功能
  7. 监控并迭代
当API灵活性和开发者体验是优先考虑因素时,GraphQL表现出色。结合TypeScript代码生成,它能提供从数据库到UI的端到端类型安全。