入门
基本查询
GraphQL 让客户端精确指定需要的数据。查询用于读取数据,响应的形状与查询一致。这避免了 REST API 中常见的过度获取和获取不足问题。
# 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 随每次调用变化。
# 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 串行执行变更(一个接一个),而查询字段并行执行。
# Mutation - write data, then return the result
mutation {
createUser(input: { name: "Bob", email: "[email protected]" }) {
id
name
}
}
# Response
{
"data": {
"createUser": {
"id": "42",
"name": "Bob"
}
}
}字段与选择集
选择集是花括号内的字段列表。标量和枚举返回叶子值;对象类型需要嵌套选择集。在对象类型上忘记写子选择集是常见的校验错误。
# 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 进行缓存。
# 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 是入口。生产服务器可禁用内省以隐藏内部细节。
# 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 } }
}
}
}Schema 与类型系统
Schema 声明
schema 块用于装配根操作类型。如果根类型命名为 Query、Mutation 和 Subscription,Apollo/GraphQL.js 会自动推断 schema,可省略此声明。
# 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)。
type Query {
user(id: ID!): User
users(limit: Int = 10): [User!]!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
type Subscription {
postAdded: Post!
}类型定义
类型用 type 关键字加字段定义。每个字段有名称和返回类型。! 后缀表示非空;[Type!] 表示非空项的列表;[Type!]! 表示非空列表且项非空。
# 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 文档和内省中。普通 # 注释仅供开发使用,会被剥离,所以面向用户的文档要用三引号字符串。
"""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 或添加模块专属字段的方式。一个类型可声明一次并扩展多次,但每个字段只能定义一次。
# 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。
# 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" });
},
});标量类型
内置标量
GraphQL 内置五个标量:Int、Float、String、Boolean 和 ID。ID 序列化为字符串,但语义上是标识符——客户端应将其视为不透明值。没有内置 Date 或 DateTime,需自行定义。
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)。否则值会原样透传。
# 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 中的内联字面量转为内部形式。三者都定义才是完整类型的标量。
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 日期解析上有诸多边界情况。
# 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 标量允许任意数据透传,适合动态配置或元数据。代价是失去类型安全和字段级校验,客户端也无法选取子字段。慎用——有类型的对象类型几乎总是更好。
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),
},
};对象类型
定义对象类型
对象类型是带返回类型的命名字段集合。对象类型构成图——它们相互引用以建立关系(User 有 Posts,Post 有 Author)。对象类型是输出类型;输入用 input 类型。
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。
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。默认用可空,仅在值确实保证存在时才用非空。
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 成为一个图,而非端点树。
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))。带默认值的参数在查询中变为可选。
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
}查询
Query 类型
Query 类型是所有读取操作的入口。这里的每个字段都是客户端可请求的顶层操作。保持它专注于读取;写操作放到 Mutation。字段用名词(users、user、search)是惯例。
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 的关键效率优势。
# 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 对象键必须唯一。别名也便于生成更短或更有意义的响应键。
# 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 查询。
query {
user(id: "1") {
name
posts { # traverse relationship
title
comments { # and another level
author {
name
}
}
}
}
}带参数的查询
参数可以是内联字面量或变量。内联字面量适合静态值;只要值来自用户输入或每次调用变化,就用变量。变量让查询可缓存,并避免字符串拼接。
# 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 中,且在一份文档中发送多个操作或使用持久化查询时是必需的。
# 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
}
}变更
Mutation 类型
Mutation 类型是写操作的入口。按约定变更字段用动词(createUser、updatePost)。变更按书写顺序串行执行,因此一次请求中多个变更字段间的顺序相关副作用是安全的。
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 类型。它们把相关字段分组,使签名简洁,并能安全演进(添加可选字段不破坏)。变更负载始终通过变量传递,绝不用内联字面量。
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。这比抛异常更灵活——可返回部分成功和结构化校验错误,与实体并列。
# 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!
}多个变更
与查询(并行)不同,一次操作中的变更按声明顺序串行执行。这保证第二个变更能看到第一个的效果。若一个变更字段出错,后续仍会执行——需校验输入并在必要时用事务。
# Multiple mutations run SERIALLY in declared order
mutation {
createPost(input: { title: "A" }) {
id
}
publishPost(id: "1") {
publishedAt
}
}幂等性
网络重试可能导致重复写入。幂等键让服务端识别重试并返回原始结果而非创建副本。这对支付、订单及任何不可逆操作至关重要。
# 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订阅
Subscription 类型
订阅在事件发生时从服务端向客户端推送数据,通常通过 WebSocket。它们是长生命周期操作。保持负载小——客户端通常用后续查询获取详细数据,而非流式传输大对象。
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 函数将发布负载转换为字段的形状。
# 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——以便事件能到达连接到任意实例的客户端。
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 让每个已连接客户端只收到相关事件——例如发给他们的消息。过滤器函数接收发布负载和订阅变量。保持过滤器轻量;它对每个已连接客户端的每个发布事件都运行。
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 clientWebSocket 传输
订阅通过 WebSocket 使用 graphql-ws(或旧版 subscriptions-transport-ws)协议。推荐较新的 graphql-ws 协议。connectionParams 携带认证令牌。服务端必须在 HTTP 端点旁安装 WS 处理器。
# 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"),
}
);参数
字段参数
参数是命名的,绝非位置参数。可出现在任何字段上,不只是根字段。命名参数使查询自文档化且与顺序无关。值动态时用变量而非内联字面量。
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 }
}默认值
带默认值的参数变为可选——客户端可省略。默认值按请求求值。非空参数没有默认值;客户端必须提供值。默认值让查询简洁,需要时又 可显式指定。
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 对象是传递复杂结构化参数的惯用方式,尤其适合过滤和分页。
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!]!
}必填与可选
参数上的 ! 后缀使其必填。省略必填参数会在任何解析器运行前校验失败,返回清晰错误。真正需要的输入用必填,可调优的行为用带默认值的可选。
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!]! 表示非空列表且元素非空——客户端必须传列表且每个元素非空。列表通过变量传递而非内联字面量,更整洁并避免转义问题。
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"] }变量
变量定义
变量使操作参数化。在操作头部以 $name: Type 声明,在正文中以 $name 引用,并在单独的 JSON variables 字段中发送值。这把静态查询与动态数据分离。
# 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 对象)——绝不能是输出对象类型。
query Search(
$term: String!
$limit: Int = 10
$filters: PostFilter
$tags: [String!]
) {
search(term: $term, limit: $limit, filters: $filters, tags: $tags) {
title
}
}默认值
变量默认值在客户端省略变量时应用。它们只对可空变量合法——非空变量意味着客户端必须提供值。默认值让客户端发送最小变量集,同时服务端保持灵活。
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.必填变量
非空变量(!)是必填的——省略它会在解析器 运行前校验失败。这是保证输入存在的最安全方式。非空变量不能有默认值;若想要默认值,就把变量设为可空。
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 测试或条件区段很棒——全部用一份可缓存的查询文档。
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片段
命名片段
片段是绑定到特定类型(fragment Name on Type)的可复用选择集。用 ...Name 在该类型出现处展开。它们让查询保持 DRY,并允许客户端跨操作共享字段选择。
# Define a reusable fragment
fragment UserFields on User {
id
name
email
}
# Spread it into queries
query {
user(id: "1") {
...UserFields
}
users {
...UserFields
}
}片段组合
片段可展开其他片段,构建组合选择。这把字段选择模块化(基本 vs 完整资料),并让每个 UI 组件声明自己的数据需求,再组合成一次查询。
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 指令支持此模式。
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 都支持)。
// 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 上展开是校验错误。用带类型条件的内联片段在联合/接 口上条件展开。还要避免循环的片段展开。
# 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).内联片段
内联片段基础
内联片段是以 ... on Type 前缀的匿名选择集。它仅在运行时对象匹配类型条件时应用字段。这是访问接口和联合类型上类型专属字段的方式。
# 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 语法是类型条件。一个选择中多个内联片段让你分别处理联合/接口的每个可能类型。所有类型共享的字段可在片段外直接选取。
query Search($term: String!) {
search(term: $term) {
... on User { name email }
... on Post { title author { name } }
... on Comment { body }
}
}访问联合类型
联合类型除 __typename 外没有共享字段,所以读取类型专属数据必须用内联片段。__typename 始终可选,告诉客户端每个项的具体类型——对渲染联合结果至关重要。
union SearchResult = User | Post | Comment
query {
search(term: "graphql") {
# shared __typename is always available
__typename
... on User { name }
... on Post { title }
... on Comment { body }
}
}访问接口
接口声明共享字段。这些共享字段可在接口上直接选取;类型专属字段需要内联片段。与联合不同,接口保证每个实现类型必须提供一组公共字段。
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 }
}
}带类型条件的命名片段
命名片段自带类型条件,所以可在联合/接口结果上展开,每个只应用到匹配类型。这把可复用性与条件选择结合——展开多个命名片段覆盖所有联合成员。
# 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.指令
内置指令
GraphQL 内置两个客户端指令:@skip(if: Boolean!) 在 true 时省略字段,@include(if: Boolean!) 仅在 true 时包含。它们互斥 ,可应用于字段、片段展开和内联片段。
# @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 变量,所以同一持久化查询可根据变量渲染摘要或详细视图——无需维护两份查询字符串。
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。
# 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 构建时校验。
# 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 或所需角色等行为。
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
}枚举
定义枚举
枚举是有限的命名值集合。枚举是标量类型——它们是无子选择的叶子值。对任何有固定选项集合 的字段(status、role、kind)用枚举而非魔法字符串,获得校验和自文档化。
enum Role {
ADMIN
USER
GUEST
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
}
type User {
role: Role!
}
type Post {
status: PostStatus!
}枚举值
查询中枚举值写作不带引号的标识符(color: RED),但在 JSON 变量和响应中序列化为字符串。命名约定为 UPPER_SNAKE_CASE。服务端会校验任何值是否在声明集合内。
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" }查询中的枚举用法
枚举既可作字段类型也可作参数类型。作参数时,客户端传递声明值之一。无效枚举值在解析器运行前校验失败,给出清晰错误。
type Query {
posts(status: PostStatus!): [Post!]!
}
query {
posts(status: PUBLISHED) { title }
}
query($status: PostStatus!) {
posts(status: $status) { title }
}
# { "status": "DRAFT" }枚举解析器
默认枚举值映射到自身。枚举解析器让你在公共 API 名(ADMIN)和内部表示(字符串 "admin" 或数字角色码)间映射。这把 schema 与存储细节解耦。
# 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 并启用更好工具。要不破坏客户端地移除枚举值,先弃用。移除客户端仍在发送的值是破坏性变更。
# 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
}接口
定义接口
接口是声明一组实现类型必须提供字段的抽象类型。它定义契约。接口适用于共享抽象,如 Node(relay 风格全局 ID)或相关类型间的 Character。
interface Node {
id: ID!
}
interface Character {
name: String!
}
type User implements Node & Character {
id: ID!
name: String!
email: String!
}实现接口
类型用 implements 关键字声明实现接口。它必须定义接口声明的每个字段,类型兼容。一个类型可实现多个接口(用 & 分隔)。schema 在构建时校验这一点。
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)。每个结果的运行时类型决定应用哪些内联片段。
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 无法在运行时确定具体类型。
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。查询返回接口类型,客户端用内联片段获取具体字段。
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.联合类型
定义联合类型
联合类型是可为若干具体类型之一的抽象类型,不要求共享字段(与接口不同)。用 union Name = TypeA | TypeB 定义。联合适合结果异构但概念相关的情况。
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 接口
成员共享公共契约(每个成员都有的字段)时用接口。成员除概念外无关——不共享字段——时用联合。一个类型可属于多个联合;联合不能直接实现其他联合。
# 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 始终可用,告诉客户端每个项的具体类型。在联合上直接选取字段(无类型条件)是校验错误。
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 中的类型对象。
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,不能用类型别名。)
# 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!
}输入类型
输入对象类型
input 类型是用于参数的对象类型,用 input 声明。它们把相关字段(一个用户、一个过滤器)分组为单个参数。与输出对象类型不同,input 类型只能包含输入类型(标量、枚举、其他 input 类型)。
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 输入。不要复用一个做两者;会破坏演进。
# 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 校验,对畸形输入给出清晰错误。
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 字段和参数可有默认值。客户端省略字段时应用默认值。默认值使输入可选,无需强迫客户端发送完整对象。注意默认值是静态字面量,不按请求计算 。
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)做声明式规则。
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");
}
// ...
}解析器
解析器函数
解析器是为一个字段产生值的函数。解析器映射与 schema 对应:resolvers.Query.user 处理 user 字段。若缺少解析器,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 用于子选择检查等高级场景。
// (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 运行。这种自顶向下流程让每个字段独立解析——图逐字段组装。
# 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 提升可读性。同层解析器并行运行(查询中),所以独立异步工作自然批量。
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)。只需为需要逻辑或获取的字段写解析器。当父对象已有正确形状时默认行为即可工作。
# 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 逻辑。
# 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.Context 与 DataLoader
Context 对象
context 是在一次操作的所有解析器间共享的每请求对象。把共享依赖放这里:数据库客户端、已认证用户、数据加载器和日志器。context 函数每请求运行一次,其返回值传给每个解析器。
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 本身每请求构建一 次,所以每请求设置放这里。
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 解析器每个用户运行一次。结果是冗余查询泛滥。
# 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 并调用批函数一次。
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 同时保持解析器简单。
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。
// 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.错误处理
错误响应格式
错误返回在顶层 errors 数组中,与 data 并列。每个错误有 message、查询中的 location、失败字段的 path,以及用于机器可读代码的 extensions。字段错误使该字段为 null(非空则冒泡),data 仍返回成功部分。
# 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 中携带错误代码。
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 中的代码让客户端可靠地按错误分支。
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(可能变化)。生产响应中不要包含堆栈跟踪。
# 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。非空是强契约——刻意使用。
# 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": "..." }]
}Apollo Server 与集成
Apollo Server 搭建
Apollo Server 是最流行的 Node.js GraphQL 服务器。传入 typeDefs(SDL)和 resolvers 构造,再用 standalone 启动以便快速开发。生产环境应与现有 HTTP 框架(Express、Fastify、Next.js)集成而非 standalone。
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 应用。
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 转换、合并多模块和应用自定义指令——在大型代码库有用。
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 内抛异常可在任何解析器运行前拒绝请求(如坏认证)。
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,生产环境禁用内省。
# 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 等阶段——适合日志、缓存和指标。
# 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.相关 GraphQL 代码片段
Copy-paste ready code for common tasks.
Schema and Types
Define types, enums, and scalars.
Queries
Read data with selection sets.
Mutations
Write data and return the result.
Subscriptions
Receive pushed updates over a stream.
Fragments
Reusable selection sets.
Variables
Parameterize operations dynamically.
Directives
Conditionally include or skip fields.
Resolvers
Functions that fulfill each field.
这篇内容对您有帮助吗?