Skip to content

GraphQL 速查表

用于 API 的查询语言和运行时。

01

入门

基本查询

GraphQL 让客户端精确指定需要的数据。查询用于读取数据,响应的形状与查询一致。这避免了 REST API 中常见的过度获取和获取不足问题。

graphql
# Query - read data
query {
  user(id: 1) {
    id
    name
    email
    posts {
      title
      content
    }
  }
}

# Response
{
  "data": {
    "user": {
      "id": 1,
      "name": "Alice",
      "email": "[email protected]",
      "posts": [
        { "title": "Hello", "content": "World" }
      ]
    }
  }
}

带变量的查询

变量使查询参数化,同一个操作可用不同输入复用。变量在操作头部以 $name: Type 声明。查询字符串保持静态,只有 variables JSON 随每次调用变化。

graphql
# Declare variables with operation name
query GetUser($id: ID!, $withPosts: Boolean!) {
  user(id: $id) {
    name
    posts @include(if: $withPosts) {
      title
    }
  }
}

# Variables sent alongside the query
{
  "id": "1",
  "withPosts": true
}

变更基础

变更用于修改数据,像查询一样返回一个选择集。按约定,变更后应跟随读取已变更的数据。GraphQL 串行执行变更(一个接一个),而查询字段并行执行。

graphql
# Mutation - write data, then return the result
mutation {
  createUser(input: { name: "Bob", email: "[email protected]" }) {
    id
    name
  }
}

# Response
{
  "data": {
    "createUser": {
      "id": "42",
      "name": "Bob"
    }
  }
}

字段与选择集

选择集是花括号内的字段列表。标量和枚举返回叶子值;对象类型需要嵌套选择集。在对象类型上忘记写子选择集是常见的校验错误。

graphql
# You must select fields - GraphQL never returns whole objects
query {
  user(id: 1) {
    # this block is a "selection set"
    name
    email
  }
}

# Selecting a scalar returns its value directly
# Selecting an object REQUIRES a sub-selection set
query {
  user(id: 1) {
    name          # scalar - OK
    posts {       # object - needs { ... }
      title
    }
  }
}

GraphQL 与 REST 对比

REST 暴露多个固定形状的端点;GraphQL 暴露一个端点,由客户端定义形状。这减少了往返次数并防止过度/不足获取。代价是服务端更复杂,且需要通过 POST 进行缓存。

graphql
# REST: multiple round trips, fixed shapes
GET /users/1            -> { id, name, email, address, ... }
GET /users/1/posts      -> [ { id, title, body, author, ... } ]

# GraphQL: one request, client-defined shape
query {
  user(id: 1) {
    name
    posts { title }
  }
}

# One endpoint, one POST request
POST /graphql
{ "query": "...", "variables": { ... } }

内省

内省让客户端在运行时发现 schema。GraphiQL、Apollo Studio 和代码生成工具都依赖它。根字段 __schema 和 __type 是入口。生产服务器可禁用内省以隐藏内部细节。

graphql
# Ask the schema what types and fields it has
{
  __schema {
    types {
      name
      kind
    }
  }
}

# Inspect a specific type's fields
{
  __type(name: "User") {
    name
    fields {
      name
      type { name kind ofType { name kind } }
    }
  }
}
02

Schema 与类型系统

Schema 声明

schema 块用于装配根操作类型。如果根类型命名为 Query、Mutation 和 Subscription,Apollo/GraphQL.js 会自动推断 schema,可省略此声明。

graphql
# SDL - Schema Definition Language
schema {
  query: Query
  mutation: Mutation
  subscription: Subscription
}

# In most servers the schema is inferred from the
# Query/Mutation/Subscription types, so this block is optional.

根类型

Query、Mutation 和 Subscription 是 schema 的入口。Query 是唯一必需的根类型。每个根字段都是客户端可执行的操作。按约定查询用名词,变更用动词(createUser)。

graphql
type Query {
  user(id: ID!): User
  users(limit: Int = 10): [User!]!
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

type Subscription {
  postAdded: Post!
}

类型定义

类型用 type 关键字加字段定义。每个字段有名称和返回类型。! 后缀表示非空;[Type!] 表示非空项的列表;[Type!]! 表示非空列表且项非空。

graphql
# Object type - the most common building block
type User {
  id: ID!
  name: String!
  age: Int
  email: String
  posts: [Post!]!
}

type Post {
  id: ID!
  title: String!
  author: User!
}

添加描述

三引号字符串(或定义前的单行字符串)会成为 schema 中的描述,显示在 GraphiQL 文档和内省中。普通 # 注释仅供开发使用,会被剥离,所以面向用户的文档要用三引号字符串。

graphql
"""A registered user of the platform."""
type User {
  """The unique, immutable identifier."""
  id: ID!

  "Display name shown publicly."
  name: String!

  # plain # comments are NOT exposed to clients
  email: String
}

Schema 扩展

extend type 为别处声明的类型添加字段。这是跨文件拆分大 schema 或添加模块专属字段的方式。一个类型可声明一次并扩展多次,但每个字段只能定义一次。

graphql
# Extend an existing type without redefining it
type User {
  id: ID!
  name: String!
}

extend type User {
  age: Int
  posts: [Post!]!
}

# Useful for splitting a schema across files,
# or for adding fields only in certain modules.

Schema 优先 vs 代码优先

Schema 优先手写 SDL 并附加解析器(Apollo 经典方式)。代码优先用 TypeScript 装饰器/构建器(Nexus、TypeGraphQL、Pothos)编程式构建 schema,获得 schema 与解析器间的类型安全。两者生成相同的运行时 schema。

graphql
# 1) Schema-first: write SDL, bind resolvers
const typeDefs = `
  type Query { hello: String }
`;
const resolvers = { Query: { hello: () => "world" } };
const server = new ApolloServer({ typeDefs, resolvers });

# 2) Code-first: build with builders (Nexus, TypeGraphQL)
export const Query = queryType({
  definition(t) {
    t.string("hello", { resolve: () => "world" });
  },
});
03

标量类型

内置标量

GraphQL 内置五个标量:Int、Float、String、Boolean 和 ID。ID 序列化为字符串,但语义上是标识符——客户端应将其视为不透明值。没有内置 Date 或 DateTime,需自行定义。

graphql
type Example {
  id: ID!          # unique identifier, serialized as string
  name: String!    # UTF-8 text
  age: Int         # 32-bit integer
  score: Float     # double-precision float
  active: Boolean! # true / false
}

自定义标量

自定义标量用于表示领域原语。声明后必须提供解析器来序列化(输出 JSON)、解析(来自变量),可选地字面量解析(来自内联 AST)。否则值会原样透传。

graphql
# Declare a custom scalar in SDL
scalar DateTime
scalar URL
scalar JSON

type Event {
  id: ID!
  at: DateTime!
  link: URL
  metadata: JSON
}

标量解析器

标量解析器有三个可选方法。serialize 将内部值转为 JSON 输出。parseValue 将变量输入转为内部形式。parseLiteral 将查询 AST 中的内联字面量转为内部形式。三者都定义才是完整类型的标量。

graphql
const resolvers = {
  DateTime: {
    // value -> JSON sent to client
    serialize: (value: Date) => value.toISOString(),
    // variable value -> internal value
    parseValue: (value: string) => new Date(value),
    // inline AST literal -> internal value
    parseLiteral: (ast) => {
      if (ast.kind === "StringValue") {
        return new Date(ast.value);
      }
      return null;
    },
  },
};

Date/DateTime 标量

graphql-scalars 包提供经过充分测试的标量:DateTime、Date、Time、EmailAddress、URL、UUID、BigInt、JSON 等。优先使用它而非手工实现,尤其在 ISO 8601 日期解析上有诸多边界情况。

graphql
# Use graphql-scalars for well-tested common types
import { DateTimeResolver } from "graphql-scalars";

const resolvers = {
  DateTime: DateTimeResolver,
  Date: DateResolver,
  Time: TimeResolver,
};

# In SDL
scalar DateTime

type Query {
  now: DateTime!
}

JSON 标量

JSON 标量允许任意数据透传,适合动态配置或元数据。代价是失去类型安全和字段级校验,客户端也无法选取子字段。慎用——有类型的对象类型几乎总是更好。

graphql
scalar JSON

type Query {
  # accept arbitrary JSON as input or output
  config: JSON
  setConfig(value: JSON): JSON
}

# The simplest pass-through resolver
const resolvers = {
  JSON: {
    serialize: (v) => v,
    parseValue: (v) => v,
    parseLiteral: (ast) => parseJsonLiteral(ast),
  },
};
04

对象类型

定义对象类型

对象类型是带返回类型的命名字段集合。对象类型构成图——它们相互引用以建立关系(User 有 Posts,Post 有 Author)。对象类型是输出类型;输入用 input 类型。

graphql
type User {
  id: ID!
  name: String!
  email: String
  createdAt: String!
}

type Post {
  id: ID!
  title: String!
  body: String!
  published: Boolean!
}

字段与字段类型

每个字段声明返回类型。可空是默认值;! 使其非空。列表用 [] 包裹。! 的位置很重要:[String] 可为空且可含 null;[String!] 可为空但项不可为 null;[String!]! 永不可为空且项不可为 null。

graphql
type Product {
  id: ID!                # non-null ID
  name: String!          # non-null string
  price: Float!          # non-null float
  discount: Float        # nullable float
  tags: [String!]!       # non-null list of non-null strings
  reviews: [Review]      # nullable list of nullable reviews
}

非空与列表

! 的位置控制每一层的可空性。非空强大但有风险:若解析器在非空字段返回 null,错误会冒泡并使父字段也为 null。默认用可空,仅在值确实保证存在时才用非空。

graphql
type Example {
  a: [String]       # may be null; items may be null
  b: [String!]      # may be null; items NOT null
  c: [String]!      # NOT null; items may be null
  d: [String!]!     # NOT null; items NOT null

  name: String      # may be null
  name2: String!    # NOT null - errors if null returned
}

嵌套对象

对象类型相互引用形成图,使查询能遍历关系(user -> posts -> author -> profile)。类型间的循环引用完全正常且常见。正是图让 GraphQL 成为一个图,而非端点树。

graphql
type User {
  id: ID!
  name: String!
  profile: Profile!
  posts: [Post!]!
}

type Profile {
  bio: String
  avatar: String
}

type Post {
  id: ID!
  title: String!
  author: User!   # back-reference creates the graph
}

对象上的字段参数

任何字段——不只是根字段——都可以接受参数。这让单个类型能暴露关联数据的过滤/分页视图(如 user.posts(limit: 10))。带默认值的参数在查询中变为可选。

graphql
type User {
  id: ID!
  name: String!
  # an object field can take arguments
  posts(limit: Int = 5, after: ID): [Post!]!
  role(type: RoleType!): Role
}

type Query {
  user(id: ID!): User
}
05

查询

Query 类型

Query 类型是所有读取操作的入口。这里的每个字段都是客户端可请求的顶层操作。保持它专注于读取;写操作放到 Mutation。字段用名词(users、user、search)是惯例。

graphql
type Query {
  me: User!
  user(id: ID!): User
  users(limit: Int = 20, offset: Int = 0): [User!]!
  search(term: String!): [SearchResult!]!
}

# Clients run these as operations:
# query { me { name } }
# query { user(id: "1") { name } }

多字段查询

一个查询可在一次请求中选取多个根字段。GraphQL 并行执行它们(mutation 除外)。这把相关获取合并为一次往返,是相对 REST 的关键效率优势。

graphql
# Several fields in one request - executed in parallel
query {
  me { name email }
  user(id: "1") { name }
  users(limit: 3) { id name }
}

# Response contains all three under "data"
{
  "data": {
    "me": { "name": "Alice", "email": "[email protected]" },
    "user": { "name": "Bob" },
    "users": [ { "id": "1", "name": "Bob" }, ... ]
  }
}

别名

别名为响应中的字段重命名。当用不同参数选取同一字段时必须使用别名——JSON 对象键必须唯一。别名也便于生成更短或更有意义的响应键。

graphql
# Same field twice with different args needs aliases
query {
  alice: user(id: "1") { name }
  bob:   user(id: "2") { name }
}

# Response keys match the aliases
{
  "data": {
    "alice": { "name": "Alice" },
    "bob":   { "name": "Bob" }
  }
}

嵌套查询

遍历关系是 GraphQL 的超能力。客户端在单次查询中遍历图(user -> posts -> comments -> author)。服务端通过解析器解析每个字段;深层嵌套没问题,但要小心后端的 N+1 查询。

graphql
query {
  user(id: "1") {
    name
    posts {            # traverse relationship
      title
      comments {        # and another level
        author {
          name
        }
      }
    }
  }
}

带参数的查询

参数可以是内联字面量或变量。内联字面量适合静态值;只要值来自用户输入或每次调用变化,就用变量。变量让查询可缓存,并避免字符串拼接。

graphql
# Schema
type Query {
  user(id: ID!, includeEmail: Boolean = false): User
}

# Inline arguments
query {
  user(id: "1", includeEmail: true) {
    name
    email
  }
}

# Prefer variables for dynamic values
query($uid: ID!, $withEmail: Boolean!) {
  user(id: $uid, includeEmail: $withEmail) {
    name
    email
  }
}

操作名称

给操作命名(query GetUserProfile)是可选但强烈推荐。命名操作更易调试,会出现在日志和 Apollo Studio 中,且在一份文档中发送多个操作或使用持久化查询时是必需的。

graphql
# Give operations a name - required in production
query GetUserProfile($id: ID!) {
  user(id: $id) {
    id
    name
    email
  }
}

mutation CreatePost($input: CreatePostInput!) {
  createPost(input: $input) {
    id
  }
}
06

变更

Mutation 类型

Mutation 类型是写操作的入口。按约定变更字段用动词(createUser、updatePost)。变更按书写顺序串行执行,因此一次请求中多个变更字段间的顺序相关副作用是安全的。

graphql
type Mutation {
  createUser(input: CreateUserInput!): User!
  updateUser(id: ID!, input: UpdateUserInput!): User!
  deleteUser(id: ID!): Boolean!
  createPost(input: CreatePostInput!): Post!
}

# Client mutation
mutation {
  createUser(input: { name: "Alice", email: "[email protected]" }) {
    id
    name
  }
}

带输入的变更

变更参数用 input 类型。它们把相关字段分组,使签名简洁,并能安全演进(添加可选字段不破坏)。变更负载始终通过变量传递,绝不用内联字面量。

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

type Mutation {
  createUser(input: CreateUserInput!): User!
}

mutation($input: CreateUserInput!) {
  createUser(input: $input) {
    id
    name
  }
}

# { "input": { "name": "Bob", "email": "[email protected]" } }

返回 Payload

常见模式是用 payload 对象包裹结果,附带 errors、success 标志或客户端变更 ID。这比抛异常更灵活——可返回部分成功和结构化校验错误,与实体并列。

graphql
# Return the changed entity AND related fields like errors
type CreateUserPayload {
  user: User
  errors: [UserError!]!
}

type UserError {
  field: String
  message: String!
}

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
}

多个变更

与查询(并行)不同,一次操作中的变更按声明顺序串行执行。这保证第二个变更能看到第一个的效果。若一个变更字段出错,后续仍会执行——需校验输入并在必要时用事务。

graphql
# Multiple mutations run SERIALLY in declared order
mutation {
  createPost(input: { title: "A" }) {
    id
  }
  publishPost(id: "1") {
    publishedAt
  }
}

幂等性

网络重试可能导致重复写入。幂等键让服务端识别重试并返回原始结果而非创建副本。这对支付、订单及任何不可逆操作至关重要。

graphql
# Use a client mutation id or idempotency key
input CreateOrderInput {
  idempotencyKey: String!
  productId: ID!
  quantity: Int!
}

type Mutation {
  createOrder(input: CreateOrderInput!): Order!
}

# Clients retry with the SAME idempotencyKey on failure
# Server returns the original order instead of creating a duplicate
07

订阅

Subscription 类型

订阅在事件发生时从服务端向客户端推送数据,通常通过 WebSocket。它们是长生命周期操作。保持负载小——客户端通常用后续查询获取详细数据,而非流式传输大对象。

graphql
type Subscription {
  postAdded: Post!
  userJoined(roomId: ID!): User!
  messageReceived(userId: ID!): Message!
}

# Client subscribes (usually over WebSocket)
subscription {
  postAdded {
    id
    title
    author { name }
  }
}

订阅事件

订阅解析器暴露一个 subscribe 函数返回 AsyncIterator。变更(或任何服务端代码)通过 pubsub 向同一主题发布事件。可选的 resolve 函数将发布负载转换为字段的形状。

graphql
# Server-side resolver with asyncIterator
const resolvers = {
  Subscription: {
    postAdded: {
      subscribe: () => pubsub.asyncIterator(["POST_ADDED"]),
      resolve: (payload) => payload.postAdded,
    },
  },
};

# Trigger from a mutation
const resolvers = {
  Mutation: {
    createPost: async (_, args, { pubsub }) => {
      const post = await createPost(args);
      await pubsub.publish("POST_ADDED", { postAdded: post });
      return post;
    },
  },
};

PubSub

graphql-subscriptions 的 PubSub 适用于单服务器实例。对于负载均衡后的多实例,需要共享消息代理——redis-pubsub、MQTT 或基于 Kafka 的 pubsub——以便事件能到达连接到任意实例的客户端。

graphql
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();

// publish an event
pubsub.publish("POST_ADDED", { postAdded: post });

// consume in a subscription resolver
subscribe: () => pubsub.asyncIterator(["POST_ADDED"])

# For multi-instance production, swap PubSub for
# Redis, NATS, or Kafka-based implementations.

带过滤器的订阅

withFilter 让每个已连接客户端只收到相关事件——例如发给他们的消息。过滤器函数接收发布负载和订阅变量。保持过滤器轻量;它对每个已连接客户端的每个发布事件都运行。

graphql
const resolvers = {
  Subscription: {
    messageReceived: {
      subscribe: withFilter(
        () => pubsub.asyncIterator(["MESSAGE_RECEIVED"]),
        (payload, variables) =>
          payload.messageReceived.recipientId === variables.userId
      ),
      resolve: (payload) => payload.messageReceived,
    },
  },
};

# Only events where the filter returns true reach this client

WebSocket 传输

订阅通过 WebSocket 使用 graphql-ws(或旧版 subscriptions-transport-ws)协议。推荐较新的 graphql-ws 协议。connectionParams 携带认证令牌。服务端必须在 HTTP 端点旁安装 WS 处理器。

graphql
# Client uses graphql-ws over WebSocket
import { createClient } from "graphql-ws";

const wsClient = createClient({
  url: "ws://localhost:4000/graphql",
  connectionParams: { authToken: "..." },
});

wsClient.subscribe(
  { query: "subscription { postAdded { id title } }" },
  {
    next: (data) => console.log(data),
    error: (err) => console.error(err),
    complete: () => console.log("done"),
  }
);
08

参数

字段参数

参数是命名的,绝非位置参数。可出现在任何字段上,不只是根字段。命名参数使查询自文档化且与顺序无关。值动态时用变量而非内联字面量。

graphql
type Query {
  user(id: ID!): User
  posts(limit: Int, offset: Int): [Post!]!
}

# Arguments can be inline literals
query {
  user(id: "1") { name }
  posts(limit: 10, offset: 0) { title }
}

默认值

带默认值的参数变为可选——客户端可省略。默认值按请求求值。非空参数没有默认值;客户端必须提供值。默认值让查询简洁,需要时又可显式指定。

graphql
type Query {
  # default values make arguments optional
  posts(limit: Int = 10, sort: String = "newest"): [Post!]!
  user(id: ID!, includeDeleted: Boolean = false): User
}

# These are equivalent:
query { posts { title } }
query { posts(limit: 10, sort: "newest") { title } }

参数类型

参数可以是标量、枚举、列表或 input 对象类型——绝不能是普通对象类型(那些仅用于输出)。input 对象是传递复杂结构化参数的惯用方式,尤其适合过滤和分页。

graphql
type Query {
  # scalar arguments
  byId(id: ID!): User
  byAge(min: Int!, max: Int!): [User!]!

  # enum argument
  byStatus(status: UserStatus!): [User!]!

  # input object argument
  search(filter: UserFilter!): [User!]!

  # list argument
  byIds(ids: [ID!]!): [User!]!
}

必填与可选

参数上的 ! 后缀使其必填。省略必填参数会在任何解析器运行前校验失败,返回清晰错误。真正需要的输入用必填,可调优的行为用带默认值的可选。

graphql
type Query {
  # required (non-null) - client MUST provide
  user(id: ID!): User

  # optional (nullable) - client may omit
  posts(limit: Int): [Post!]!

  # optional with default
  recentPosts(limit: Int = 5): [Post!]!
}

# Omitting a required argument is a validation error
# before any resolver runs.

列表参数

列表参数用方括号。[ID!]! 表示非空列表且元素非空——客户端必须传列表且每个元素非空。列表通过变量传递而非内联字面量,更整洁并避免转义问题。

graphql
type Query {
  usersByIds(ids: [ID!]!): [User!]!
  tags(allOf: [String!]): [Post!]!
}

# Inline list literal
query {
  usersByIds(ids: ["1", "2", "3"]) { name }
}

# Via variable
query($ids: [ID!]!) {
  usersByIds(ids: $ids) { name }
}
# { "ids": ["1", "2", "3"] }
09

变量

变量定义

变量使操作参数化。在操作头部以 $name: Type 声明,在正文中以 $name 引用,并在单独的 JSON variables 字段中发送值。这把静态查询与动态数据分离。

graphql
# Declare variables in the operation, then use them
query GetUser($id: ID!) {
  user(id: $id) {
    name
    email
  }
}

# Variables are sent in a separate JSON object
{
  "id": "1"
}

变量类型

变量类型必须匹配 schema 中的参数类型。变量可空、非空、列表或 input 对象。变量类型必须是输入类型(标量、枚举或 input 对象)——绝不能是输出对象类型。

graphql
query Search(
  $term: String!
  $limit: Int = 10
  $filters: PostFilter
  $tags: [String!]
) {
  search(term: $term, limit: $limit, filters: $filters, tags: $tags) {
    title
  }
}

默认值

变量默认值在客户端省略变量时应用。它们只对可空变量合法——非空变量意味着客户端必须提供值。默认值让客户端发送最小变量集,同时服务端保持灵活。

graphql
query Posts($limit: Int = 10, $sort: String = "newest") {
  posts(limit: $limit, sort: $sort) { title }
}

# If the client omits "limit", it defaults to 10.
# Only nullable variables can have defaults - a non-null
# variable with a default is a contradiction.

必填变量

非空变量(!)是必填的——省略它会在解析器运行前校验失败。这是保证输入存在的最安全方式。非空变量不能有默认值;若想要默认值,就把变量设为可空。

graphql
query User($id: ID!) {     # ! means required
  user(id: $id) { name }
}

# Missing required variable -> validation error
{ "id": "1" }      # OK
{ }                # ERROR: variable $id of type ID! is required

# A non-null variable cannot have a default value.

变量与指令配合

变量可驱动 @include 和 @skip 等指令,让客户端动态切换字段。这对功能开关、A/B 测试或条件区段很棒——全部用一份可缓存的查询文档。

graphql
query User($id: ID!, $withPosts: Boolean!) {
  user(id: $id) {
    name
    posts @include(if: $withPosts) {
      title
    }
  }
}

# { "id": "1", "withPosts": false }  -> posts omitted
# { "id": "1", "withPosts": true }   -> posts included
10

片段

命名片段

片段是绑定到特定类型(fragment Name on Type)的可复用选择集。用 ...Name 在该类型出现处展开。它们让查询保持 DRY,并允许客户端跨操作共享字段选择。

graphql
# Define a reusable fragment
fragment UserFields on User {
  id
  name
  email
}

# Spread it into queries
query {
  user(id: "1") {
    ...UserFields
  }
  users {
    ...UserFields
  }
}

片段组合

片段可展开其他片段,构建组合选择。这把字段选择模块化(基本 vs 完整资料),并让每个 UI 组件声明自己的数据需求,再组合成一次查询。

graphql
fragment UserBasic on User {
  id
  name
}

fragment UserWithEmail on User {
  ...UserBasic
  email
}

fragment UserFull on User {
  ...UserWithEmail
  posts { title }
}

# Fragments can spread other fragments of the same type.

片段变量

Relay 风格的片段变量让片段接受参数(如 limit),调用方通过 @arguments 传递。这让片段真正可用不同设置复用。Apollo 通过其客户端指令的 @arguments 指令支持此模式。

graphql
fragment UserPosts on User
  @argumentDefinitions(limit: { type: "Int", defaultValue: 5 }) {
  posts(limit: $limit) {
    title
  }
}

query {
  user(id: "1") {
    ...UserPosts @arguments(limit: 10)
  }
}

# @argumentDefinitions and @arguments come from
# Relay; Apollo uses a similar pattern via client directives.

可复用片段

把片段与渲染它的组件放在一起是强大模式:每个组件声明数据需求,父组件组合成一次查询。这在大型应用中扩展数据获取(Relay 和 Apollo 都支持)。

graphql
// Colocate fragments with the UI component that uses them
const USER_CARD = graphql`
  fragment UserCard_user on User {
    id
    name
    avatarUrl
  }
`;

function UserCard({ user }) {
  return <div>{user.name}</div>;
}

// The parent query spreads all needed fragments
query {
  users {
    ...UserCard_user
  }
}

片段注意事项

在 Post 上声明的片段只能在期望 Post 处展开——在 User 上展开是校验错误。用带类型条件的内联片段在联合/接口上条件展开。还要避免循环的片段展开。

graphql
# A fragment must match the type where it's spread
fragment PostFields on Post { title body }

query {
  user(id: "1") {
    ...PostFields   # ERROR: user is not a Post
  }
}

# Fragments can only spread on the declared type
# (or via type conditions with inline fragments).
11

内联片段

内联片段基础

内联片段是以 ... on Type 前缀的匿名选择集。它仅在运行时对象匹配类型条件时应用字段。这是访问接口和联合类型上类型专属字段的方式。

graphql
# An inline fragment is a selection set with a type condition
query {
  user(id: "1") {
    id
    name
    # fields common to User
    ... on AdminUser {
      permissions
    }
  }
}

# No name - declared inline in the query.

类型条件

... on Type 语法是类型条件。一个选择中多个内联片段让你分别处理联合/接口的每个可能类型。所有类型共享的字段可在片段外直接选取。

graphql
query Search($term: String!) {
  search(term: $term) {
    ... on User { name email }
    ... on Post { title author { name } }
    ... on Comment { body }
  }
}

访问联合类型

联合类型除 __typename 外没有共享字段,所以读取类型专属数据必须用内联片段。__typename 始终可选,告诉客户端每个项的具体类型——对渲染联合结果至关重要。

graphql
union SearchResult = User | Post | Comment

query {
  search(term: "graphql") {
    # shared __typename is always available
    __typename
    ... on User { name }
    ... on Post { title }
    ... on Comment { body }
  }
}

访问接口

接口声明共享字段。这些共享字段可在接口上直接选取;类型专属字段需要内联片段。与联合不同,接口保证每个实现类型必须提供一组公共字段。

graphql
interface Node { id: ID! }
type User implements Node { id: ID! name: String! }
type Post implements Node { id: ID! title: String! }

query {
  node(id: "1") {
    id              # shared interface field - selectable directly
    ... on User { name }
    ... on Post { title }
  }
}

带类型条件的命名片段

命名片段自带类型条件,所以可在联合/接口结果上展开,每个只应用到匹配类型。这把可复用性与条件选择结合——展开多个命名片段覆盖所有联合成员。

graphql
# Named fragments can also carry a type condition
fragment UserFields on User { name email }
fragment PostFields on Post { title }

query {
  search(term: "a") {
    ...UserFields
    ...PostFields
  }
}

# Equivalent to inlining those fragments inline.
12

指令

内置指令

GraphQL 内置两个客户端指令:@skip(if: Boolean!) 在 true 时省略字段,@include(if: Boolean!) 仅在 true 时包含。它们互斥,可应用于字段、片段展开和内联片段。

graphql
# @skip - omit field if true
query($skip: Boolean!) {
  user(id: "1") {
    name
    email @skip(if: $skip)
  }
}

# @include - include field only if true
query($withPosts: Boolean!) {
  user(id: "1") {
    posts @include(if: $withPosts) { title }
  }
}

@skip 与 @include

@skip 和 @include 让一份查询服务多种形状。它们接受 Boolean 变量,所以同一持久化查询可根据变量渲染摘要或详细视图——无需维护两份查询字符串。

graphql
query User($id: ID!, $detailed: Boolean!) {
  user(id: $id) {
    name
    email @skip(if: $detailed)      # hide when detailed
    profile @include(if: $detailed) {  # show when detailed
      bio
    }
  }
}

# { "id": "1", "detailed": false } -> name, email
# { "id": "1", "detailed": true }  -> name, profile { bio }

自定义指令

自定义指令以 directive 声明开始,给出参数和允许位置。执行语义并非内置——用 schema 转换(Apollo)或指令访问器(graphql-tools)实现。常见用途:@auth、@cached、@deprecated。

graphql
# Declare a directive in SDL
directive @auth(requires: Role = ADMIN) on FIELD_DEFINITION

type Query {
  adminData: String @auth(requires: ADMIN)
  userData: String @auth(requires: USER)
}

# A directive needs a location: FIELD_DEFINITION, FIELD,
# OBJECT, ENUM, etc. Logic is implemented in the server
# (e.g. via schema transforms or a directive visitor).

指令位置

每个指令声明其合法位置——操作、字段、片段或 schema 定义(FIELD_DEFINITION、OBJECT 等)。客户端指令(查询中应用)和 schema 指令(SDL 中应用)使用不同的位置集合。在 schema 构建时校验。

graphql
# Where a directive can appear
directive @example on
  | QUERY          # operation
  | FIELD          # a field in a selection
  | FRAGMENT_DEFINITION
  | FRAGMENT_SPREAD
  | INLINE_FRAGMENT
  | FIELD_DEFINITION  # in SDL (server-side)
  | OBJECT | INTERFACE | UNION | ENUM
  | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION;

# Multiple locations use | between them.

指令参数

指令可接受带默认值的类型化参数。内置 @deprecated(reason:) 标记字段和枚举值为过时;工具会警告使用它们的客户端。带参数的自定义指令驱动如缓存 TTL 或所需角色等行为。

graphql
directive @deprecated(
  reason: String = "No longer supported"
) on FIELD_DEFINITION | ENUM_VALUE

type Query {
  oldField: String @deprecated(reason: "Use newField")
  newField: String
}

enum Status {
  ACTIVE
  INACTIVE @deprecated(reason: "Use ARCHIVED")
  ARCHIVED
}
13

枚举

定义枚举

枚举是有限的命名值集合。枚举是标量类型——它们是无子选择的叶子值。对任何有固定选项集合的字段(status、role、kind)用枚举而非魔法字符串,获得校验和自文档化。

graphql
enum Role {
  ADMIN
  USER
  GUEST
}

enum PostStatus {
  DRAFT
  PUBLISHED
  ARCHIVED
}

type User {
  role: Role!
}

type Post {
  status: PostStatus!
}

枚举值

查询中枚举值写作不带引号的标识符(color: RED),但在 JSON 变量和响应中序列化为字符串。命名约定为 UPPER_SNAKE_CASE。服务端会校验任何值是否在声明集合内。

graphql
enum Color {
  RED
  GREEN
  BLUE
}

# In a query, enum values are unquoted identifiers
query {
  products(color: RED) { name }
}

# In variables JSON, they're sent as strings
{ "color": "RED" }

查询中的枚举用法

枚举既可作字段类型也可作参数类型。作参数时,客户端传递声明值之一。无效枚举值在解析器运行前校验失败,给出清晰错误。

graphql
type Query {
  posts(status: PostStatus!): [Post!]!
}

query {
  posts(status: PUBLISHED) { title }
}

query($status: PostStatus!) {
  posts(status: $status) { title }
}
# { "status": "DRAFT" }

枚举解析器

默认枚举值映射到自身。枚举解析器让你在公共 API 名(ADMIN)和内部表示(字符串 "admin" 或数字角色码)间映射。这把 schema 与存储细节解耦。

graphql
# Map internal values <-> API values
enum Role {
  ADMIN
  USER
}

const resolvers = {
  Role: {
    ADMIN: "admin",   // internal DB value
    USER: "user",
  },
};

# Internally the resolver sees "admin"; the client sees ADMIN.
# Useful when DB values differ from the public API.

枚举最佳实践

对固定选项集合始终优先枚举而非自由字符串——它们校验输入、文档化 API 并启用更好工具。要不破坏客户端地移除枚举值,先弃用。移除客户端仍在发送的值是破坏性变更。

graphql
# Prefer enums over strings for fixed option sets
type User {
  role: Role!          # GOOD - validated, self-documenting
  roleStr: String      # AVOID - no validation, magic strings
}

# Deprecate values instead of removing them
enum Status {
  ACTIVE
  INACTIVE @deprecated(reason: "Use ARCHIVED")
  ARCHIVED
}
14

接口

定义接口

接口是声明一组实现类型必须提供字段的抽象类型。它定义契约。接口适用于共享抽象,如 Node(relay 风格全局 ID)或相关类型间的 Character。

graphql
interface Node {
  id: ID!
}

interface Character {
  name: String!
}

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

实现接口

类型用 implements 关键字声明实现接口。它必须定义接口声明的每个字段,类型兼容。一个类型可实现多个接口(用 & 分隔)。schema 在构建时校验这一点。

graphql
interface Node {
  id: ID!
}

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

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

# A type must implement every field of the interface.

查询接口

当字段返回接口时,可直接选取接口的共享字段。访问类型专属字段用内联片段(... on User)。每个结果的运行时类型决定应用哪些内联片段。

graphql
interface Node { id: ID! }

type Query {
  node(id: ID!): Node
}

query {
  node(id: "1") {
    id              # interface field - always selectable
    ... on User { name }
    ... on Post { title }
  }
}

接口解析器

接口需要 __resolveType 函数,为给定运行时值返回具体类型名(或类型对象)。这让 GraphQL 知道应用哪些内联片段。没有它,GraphQL 无法在运行时确定具体类型。

graphql
interface Character { name: String! }

type Human implements Character {
  name: String!
  starships: [String!]!
}

type Droid implements Character {
  name: String!
  primaryFunction: String
}

const resolvers = {
  Character: {
    // tell GraphQL which concrete type a value is
    __resolveType(obj) {
      return obj.primaryFunction ? "Droid" : "Human";
    },
  },
};

多接口

一个类型可用 & 分隔符实现多个接口。这组合能力——例如一个 Article 同时是 Node、Timestamped 和 SoftDeletable。查询返回接口类型,客户端用内联片段获取具体字段。

graphql
interface Node { id: ID! }
interface Timestamped { createdAt: String! }
interface SoftDeletable { deletedAt: String }

type Article implements Node & Timestamped & SoftDeletable {
  id: ID!
  createdAt: String!
  deletedAt: String
  title: String!
}

# Use & to list multiple implemented interfaces.
15

联合类型

定义联合类型

联合类型是可为若干具体类型之一的抽象类型,不要求共享字段(与接口不同)。用 union Name = TypeA | TypeB 定义。联合适合结果异构但概念相关的情况。

graphql
union SearchResult = User | Post | Comment

type User { id: ID! name: String! }
type Post { id: ID! title: String! }
type Comment { id: ID! body: String! }

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

联合类型 vs 接口

成员共享公共契约(每个成员都有的字段)时用接口。成员除概念外无关——不共享字段——时用联合。一个类型可属于多个联合;联合不能直接实现其他联合。

graphql
# INTERFACE - shared contract
interface Node { id: ID! }
type User implements Node { id: ID! name: String! }
type Post implements Node { id: ID! title: String! }
# All members share the "id" field.

# UNION - no shared contract
union SearchResult = User | Post
# Members share NO required fields.

查询联合类型

联合不暴露自身字段,所以必须用内联片段读取任何内容。__typename 始终可用,告诉客户端每个项的具体类型。在联合上直接选取字段(无类型条件)是校验错误。

graphql
query {
  search(term: "graphql") {
    __typename          # always available
    ... on User { name email }
    ... on Post { title }
    ... on Comment { body }
  }
}

# You CANNOT select fields directly on a union
# (no shared fields) - only via inline fragments.

联合类型解析器

与接口一样,联合需要 __resolveType 解析器为运行时值返回具体类型名。没有它 GraphQL 无法选择内联片段。返回类型名字符串或 schema 中的类型对象。

graphql
union SearchResult = User | Post | Comment

const resolvers = {
  SearchResult: {
    __resolveType(obj) {
      if (obj.email) return "User";
      if (obj.title) return "Post";
      if (obj.body) return "Comment";
      return null;
    },
  },
};

常见模式

联合建模结果或错误结果:变更返回成功类型和错误类型的联合,迫使客户端处理两者。这比带可选错误字段的单一 payload 更类型安全。(SDL 要求 union,不能用类型别名。)

graphql
# Mutation result that may be success or error
type CreateUserSuccess { user: User! }
type CreateUserError { messages: [String!]! }
type CreateUserPayload = CreateUserSuccess | CreateUserError
# (note: a type alias like this isn't valid SDL; use union)
union CreateUserPayload = CreateUserSuccess | CreateUserError

type Mutation {
  createUser(input: CreateUserInput!): CreateUserPayload!
}
16

输入类型

输入对象类型

input 类型是用于参数的对象类型,用 input 声明。它们把相关字段(一个用户、一个过滤器)分组为单个参数。与输出对象类型不同,input 类型只能包含输入类型(标量、枚举、其他 input 类型)。

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

type Mutation {
  createUser(input: CreateUserInput!): User!
}

mutation($input: CreateUserInput!) {
  createUser(input: $input) { id }
}
# { "input": { "name": "Alice", "email": "[email protected]" } }

输入与输出

输出类型(type)可有字段参数和解析器;input 类型(input)是无解析器的纯数据载体。它们是独立命名空间——可同时有 User 类型和 UserInput 输入。不要复用一个做两者;会破坏演进。

graphql
# OUTPUT type - returned from resolvers, has resolvers
type User {
  id: ID!
  name: String!
  posts: [Post!]!   # can have field arguments and resolvers
}

# INPUT type - passed into operations, no resolvers
input UserInput {
  name: String!
  email: String!
  # NO field arguments, NO resolvers, NO output object types
}

# A type cannot be used as both input and output.

嵌套输入

input 类型可引用其他 input 类型和列表,建模复杂的嵌套负载。这非常适合分层表单或嵌套过滤器。每一层都对照 schema 校验,对畸形输入给出清晰错误。

graphql
input AddressInput {
  street: String!
  city: String!
  zip: String!
}

input CreateUserInput {
  name: String!
  email: String!
  address: AddressInput      # nested input type
  tags: [String!]            # list input
}

type Mutation {
  createUser(input: CreateUserInput!): User!
}

输入默认值

input 字段和参数可有默认值。客户端省略字段时应用默认值。默认值使输入可选,无需强迫客户端发送完整对象。注意默认值是静态字面量,不按请求计算。

graphql
input PaginationInput {
  limit: Int = 20
  offset: Int = 0
  sort: SortOrder = DESC
}

enum SortOrder { ASC DESC }

type Query {
  posts(page: PaginationInput = { limit: 20, offset: 0 }): [Post!]!
}

# Defaults apply per-field when omitted by the client.

输入校验

GraphQL 校验类型和非空性,但不校验语义规则(邮箱格式、年龄范围、字符串长度)。在解析器中实现这些,对坏数据抛 UserInputError。一些服务器添加校验指令(@constraint)做声明式规则。

graphql
input CreateUserInput {
  name: String!              # required, non-null
  email: String!             # format check needs custom code
  age: Int                   # range check needs custom code
}

# GraphQL validates structure; you validate semantics
function createUser(_, { input }) {
  if (!input.email.includes("@")) {
    throw new UserInputError("Invalid email");
  }
  if (input.age < 0 || input.age > 150) {
    throw new UserInputError("Invalid age");
  }
  // ...
}
17

解析器

解析器函数

解析器是为一个字段产生值的函数。解析器映射与 schema 对应:resolvers.Query.user 处理 user 字段。若缺少解析器,GraphQL 使用默认行为,从父对象读取同名属性。

graphql
const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      return context.db.user.findById(args.id);
    },
    users: () => db.users.findAll(),
  },
  Mutation: {
    createUser: (_, { input }, { db }) =>
      db.user.create(input),
  },
};

解析器参数

每个解析器有四个参数:parent(父字段解析的值)、args(该字段的参数)、context(每请求共享对象)和 info(AST 和执行元数据)。前两个最常用;info 用于子选择检查等高级场景。

graphql
// (parent, args, context, info)
const resolvers = {
  Query: {
    user: (parent, args, context, info) => {
      // parent   - value returned by the parent field (root for Query)
      // args     - arguments passed to this field
      // context  - shared per-request object (db, user, loaders)
      // info     - AST + execution info (rarely needed)
      return context.db.user.findById(args.id);
    },
  },
};

解析器链

解析器按深度优先链运行:父字段的解析器产生对象,然后每个子字段的解析器以该对象为 parent 运行。这种自顶向下流程让每个字段独立解析——图逐字段组装。

graphql
# query { user(id: "1") { name posts { title } } }
const resolvers = {
  Query: {
    user: () => ({ id: "1", name: "Alice" }),   // runs first
  },
  User: {
    posts: (user, args, ctx) => {                // runs after user
      return ctx.db.posts.findByAuthor(user.id);
    },
  },
  Post: {
    title: (post) => post.title,                 // runs after posts
  },
};

异步解析器

解析器可返回 Promise;GraphQL 在继续子字段前 await 它。这是调用数据库和 API 的方式。用 async/await 提升可读性。同层解析器并行运行(查询中),所以独立异步工作自然批量。

graphql
const resolvers = {
  Query: {
    // return a Promise - GraphQL awaits it
    user: async (_, { id }, { db }) => {
      const user = await db.user.findById(id);
      if (!user) throw new Error("User not found");
      return user;
    },
  },
  User: {
    posts: async (user, _, { db }) =>
      db.posts.findByAuthor(user.id),
  },
};

默认解析器

若字段无解析器,GraphQL 从父对象读取同名属性(undefined 时返回 null)。只需为需要逻辑或获取的字段写解析器。当父对象已有正确形状时默认行为即可工作。

graphql
# If you don't define a resolver, GraphQL reads from the parent
const resolvers = {
  User: {
    // name resolver missing - default behavior:
    // (parent) => parent.name
    posts: (user) => fetchPosts(user.id),  // only this is custom
  },
};

// Equivalent explicit default:
User: {
  name: (user) => (user.name === undefined ? null : user.name),
}

解析器最佳实践

保持解析器精简:把获取委托给 context 上的数据源类。这集中数据访问(可测试、可 mock)并让数据源批处理和缓存。解析器应编排,不应直接包含 SQL 或 fetch 逻辑。

graphql
# Keep resolvers thin - delegate to data sources
const resolvers = {
  Query: {
    user: (_, { id }, { dataSources }) =>
      dataSources.userAPI.getUser(id),
  },
  User: {
    posts: (user, _, { dataSources }) =>
      dataSources.postAPI.byAuthor(user.id),
  },
};

// dataSources live on context, are injected per request,
// and can batch/cache calls internally.
18

Context 与 DataLoader

Context 对象

context 是在一次操作的所有解析器间共享的每请求对象。把共享依赖放这里:数据库客户端、已认证用户、数据加载器和日志器。context 函数每请求运行一次,其返回值传给每个解析器。

graphql
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({
    db: new PrismaClient(),
    user: getUserFromToken(req.headers.authorization),
    loaders: buildLoaders(),
  }),
});

// Inside any resolver, the third argument is context:
(parent, args, context) => context.db.user.findById(args.id);

每请求 Context

在 context 函数内创建新的 DataLoader 实例,使其缓存限定于单次请求。跨请求复用 loader 会在用户间泄漏缓存数据。context 本身每请求构建一次,所以每请求设置放这里。

graphql
const context = async ({ req }) => {
  const token = req.headers.authorization || "";
  const user = await verifyToken(token);  // may be null

  return {
    user,
    db,
    // a FRESH DataLoader instance per request
    // so caches don't leak across requests
    userLoader: new DataLoader((ids) => batchGetUsers(ids)),
  };
};

N+1 问题

N+1 问题:获取 N 个项的列表,然后每个项发一个额外查询加载关联。GraphQL 的并行字段解析让这极易发生——User.posts 解析器每个用户运行一次。结果是冗余查询泛滥。

graphql
# query { users { id name posts { title } } }
// BAD: 1 query for users, then N queries for each user's posts
const resolvers = {
  User: {
    posts: (user, _, { db }) =>
      db.posts.findByAuthor(user.id),  // runs once per user!
  },
};

// If there are 100 users, that's 101 DB queries.
// This is the classic N+1 problem.

DataLoader 基础

DataLoader 把多个单独 load 合并为一次批量请求。你给它一个批函数,接受键数组并返回同序结果数组。DataLoader 然后收集一个 tick 内所有 load 并调用批函数一次。

graphql
import DataLoader from "dataloader";

// A batch function receives an array of keys, returns
// an array of values IN THE SAME ORDER as the keys.
const userLoader = new DataLoader(async (userIds) => {
  const users = await db.users.findMany({ where: { id: { in: userIds } } });
  // reorder to match the input keys
  return userIds.map((id) =>
    users.find((u) => u.id === id) ?? null
  );
});

解析器中使用 DataLoader

在解析器中用 loader.load(key) 而非直接 DB 调用。一个 tick 内同一 loader 的所有 load 批处理为一次查询,结果缓存持续整个请求。这消除 N+1 同时保持解析器简单。

graphql
const resolvers = {
  User: {
    posts: (user, _, { loaders }) =>
      loaders.postLoader.load(user.id),
  },
  Post: {
    author: (post, _, { loaders }) =>
      loaders.userLoader.load(post.authorId),
  },
};

// 100 users -> ONE batched query for all their posts,
// instead of 100 separate queries.

批处理与缓存

DataLoader 做两件事:批处理(一个事件循环 tick 内多个 load 变成一次批调用)和按键缓存(同键重复 load 返回同一 Promise)。缓存是每 loader 实例的,这就是为何每请求创建新 loader。

graphql
// Batching: many .load() calls in one tick -> one batch fn call
await Promise.all([loader.load(1), loader.load(2), loader.load(3)]);
// -> batchGetUsers([1, 2, 3]) called ONCE

// Caching: repeated .load(sameKey) returns the cached Promise
loader.load(1); loader.load(1); loader.load(1);
// -> batchGetUsers([1]) called ONCE; later calls hit cache

// .clear(key) invalidates; .clearAll() resets everything.
19

错误处理

错误响应格式

错误返回在顶层 errors 数组中,与 data 并列。每个错误有 message、查询中的 location、失败字段的 path,以及用于机器可读代码的 extensions。字段错误使该字段为 null(非空则冒泡),data 仍返回成功部分。

graphql
# Errors appear in a top-level "errors" array
{
  "errors": [
    {
      "message": "User not found",
      "locations": [{ "line": 2, "column": 3 }],
      "path": ["user"],
      "extensions": {
        "code": "NOT_FOUND",
        "exception": { "stacktrace": ["..."] }
      }
    }
  ],
  "data": { "user": null }
}

抛出错误

最简单的错误处理:抛出 Error。GraphQL 捕获它,使字段为 null,并在 errors 数组添加一项。要更结构化,用专门的错误类(ApolloError、UserInputError),它们在 extensions 中携带错误代码。

graphql
const resolvers = {
  Query: {
    user: async (_, { id }, { db }) => {
      const user = await db.user.findById(id);
      if (!user) {
        throw new Error("User not found");
      }
      return user;
    },
  },
};

# Thrown errors become entries in the "errors" array.

自定义错误

用类型化错误类传达意图:UserInputError 表示坏客户端输入(4xx),AuthenticationError 表示缺认证,ForbiddenError 表示无权限,ApolloError 带代码用于自定义场景。extensions 中的代码让客户端可靠地按错误分支。

graphql
import { ApolloError, UserInputError } from "apollo-server-errors";

const resolvers = {
  Mutation: {
    createUser: (_, { input }) => {
      if (!isValidEmail(input.email)) {
        throw new UserInputError("Invalid email", {
          field: "email",
        });
      }
      if (emailTaken(input.email)) {
        throw new ApolloError("Email taken", "EMAIL_TAKEN", {
          email: input.email,
        });
      }
      // ...
    },
  },
};

错误扩展信息

extensions 携带机器可读数据:稳定的 code 字符串、重试提示、校验字段路径等。客户端应 switch extensions.code 而非解析人类可读 message(可能变化)。生产响应中不要包含堆栈跟踪。

graphql
# Structured, machine-readable error data
throw new ApolloError("Rate limited", "RATE_LIMITED", {
  code: "RATE_LIMITED",
  retryAfter: 60,
  limit: 100,
});

# Clients check extensions.code to handle errors
if (err.extensions.code === "RATE_LIMITED") {
  wait(err.extensions.retryAfter);
}

部分结果

GraphQL 返回部分结果:成功字段在 data 中,失败字段为 null(非空则冒泡)并列在 errors 中。优先用可空字段,这样一个失败不会使整个响应为 null。非空是强契约——刻意使用。

graphql
# A non-null field error nulls the parent; nullable just nulls the field
type Query {
  user(id: ID!): User       # nullable - error nulls only "user"
  me: User!                 # non-null - error bubbles up to null "me"
}

# Query selecting multiple fields:
query { me { name } user(id: "bad") { name } }
# If "user" errors but "me" succeeds:
{
  "data": { "me": { "name": "Alice" }, "user": null },
  "errors": [{ "path": ["user"], "message": "..." }]
}
20

Apollo Server 与集成

Apollo Server 搭建

Apollo Server 是最流行的 Node.js GraphQL 服务器。传入 typeDefs(SDL)和 resolvers 构造,再用 standalone 启动以便快速开发。生产环境应与现有 HTTP 框架(Express、Fastify、Next.js)集成而非 standalone。

graphql
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";

const typeDefs = `#graphql
  type Query { hello: String }
`;

const resolvers = {
  Query: { hello: () => "Hello, world!" },
};

const server = new ApolloServer({ typeDefs, resolvers });
const { url } = await startStandaloneServer(server, {
  listen: { port: 4000 },
});
console.log(`Server ready at ${url}`);

Express 集成

Express 用 @apollo/server/express4 的 expressMiddleware。挂载中间件前先启动服务器。context 函数接收 Express 的 req/res,可读 header、session 或附加认证。这把 GraphQL 组合进现有 Express 应用。

graphql
import { ApolloServer } from "@apollo/server";
import { expressMiddleware } from "@apollo/server/express4";
import express from "express";
import cors from "cors";
import bodyParser from "body-parser";

const app = express();
const server = new ApolloServer({ typeDefs, resolvers });
await server.start();

app.use("/graphql", cors(), bodyParser.json(),
  expressMiddleware(server, {
    context: async ({ req }) => ({ user: getUser(req) }),
  })
);
app.listen(4000);

Schema 构建

makeExecutableSchema(来自 @graphql-tools/schema)从 typeDefs 和 resolvers 构建 schema,返回 GraphQLSchema 对象传给 ApolloServer。这层间接支持 schema 转换、合并多模块和应用自定义指令——在大型代码库有用。

graphql
import { makeExecutableSchema } from "@graphql-tools/schema";

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

const server = new ApolloServer({ schema });

// makeExecutableSchema lets you apply schema transforms,
// directive visitors, and merge modules before serving.

Context 函数

context 函数每请求运行一次,其返回值由所有解析器共享。在这里读认证 header 并附加已认证用户、数据库客户端和新的 DataLoader。在 context 内抛异常可在任何解析器运行前拒绝请求(如坏认证)。

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

const { url } = await startStandaloneServer(server, {
  context: async ({ req }) => {
    const token = req.headers.authorization || "";
    const user = await verifyToken(token);
    return { user, db, loaders: makeLoaders() };
  },
  listen: { port: 4000 },
});

Apollo Studio / Sandbox

Apollo Sandbox(在浏览器中访问服务器 URL)让你在开发时探索 schema 和运行操作。Apollo Studio 是云对应物,提供 schema 历史、指标和追踪。若想对外隐藏 schema,生产环境禁用内省。

graphql
# Apollo Sandbox is a dev IDE in the browser
# Visit the server URL in a browser to open it.

const server = new ApolloServer({
  typeDefs,
  resolvers,
  // Apollo Studio reports go through this key (optional)
  // apollo: { graphRef: "my-graph@current" },
});

# For introspection in production, enable it explicitly:
# new ApolloServer({ introspection: true });

请求生命周期

Apollo Server 每请求运行生命周期:构建 context、解析查询、对照 schema 校验、执行解析器,然后格式化响应。formatError 自定义错误形状;plugins 钩入 didResolveOperation、willSendResponse 等阶段——适合日志、缓存和指标。

graphql
# Order: HTTP request -> context -> parse -> validate ->
#        execute (resolvers) -> format response -> HTTP response

const server = new ApolloServer({
  typeDefs,
  resolvers,
  formatError: (formatted, error) => ({
    message: error.message,
    code: formatted.extensions?.code,
  }),
  plugins: [loggingPlugin, cachePlugin],
});

# plugins hook into every lifecycle phase.

这篇内容对您有帮助吗?

学习路径

从零开始学习

通过结构化课程从头学习这个语言。