graphql
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGraphQL 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
undefinedgraphql
undefinedschema.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!
}
undefinedtype 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!
}
undefined2. 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
undefinedgraphql
undefinedQuery - 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
}
}
}
undefinedsubscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}
undefinedFields 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 }
}
}
}
undefinedquery Search {
search(query: "graphql") {
... on User {
name
email
}
... on Post {
title
content
}
... on Comment {
text
author { name }
}
}
}
undefinedEnums
枚举类型
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
undefinedgraphql
undefinedBuilt-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
undefinedscalar DateTime # ISO 8601时间戳
scalar Email # 邮箱地址
scalar URL # 有效URL
scalar JSON # 任意JSON数据
scalar Upload # 文件上传
undefinedCustom 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
undefinedgraphql
undefinedDefine 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
}
}
}
undefinedquery {
user(id: "1") {
...UserFields
posts {
...PostSummary
}
}
}
undefinedDirectives
指令
graphql
undefinedgraphql
undefinedBuilt-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
}
undefineddirective @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User! @auth
}
undefinedMutations Best Practices
变更最佳实践
graphql
undefinedgraphql
undefinedSingle 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!undefinedAdvanced 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 valuetypescript
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>;
}undefinedRedis 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
undefinedError 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);
undefinedSchema 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>;
}
undefinedStrawberry (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,
},
});
}
}
undefinedgraphql-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-apollotypescript
undefinedcodegen.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: truetype 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-apolloProduction 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: trueApollo 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
—
| Feature | GraphQL | REST |
|---|---|---|
| Endpoints | Single endpoint | Multiple endpoints |
| Data Fetching | Client specifies fields | Server determines response |
| Over-fetching | No | Yes (extra fields) |
| Under-fetching | No | Yes (multiple requests) |
| Versioning | Not needed | Required (v1, v2) |
| Caching | Complex | Simple (HTTP caching) |
| Type System | Built-in | External (OpenAPI) |
| Real-time | Subscriptions | SSE/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
性能优化
—
查询复杂度分析
| Feature | GraphQL | tRPC |
|---|---|---|
| Type Safety | Codegen required | Native TypeScript |
| Language Support | Any | TypeScript only |
| Client-Server Coupling | Loose | Tight |
| Schema | SDL required | Inferred from code |
| Learning Curve | Steep | Gentle |
| Tooling | Extensive | Growing |
| Use Case | Public APIs, Mobile | Full-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 readytypescript
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));undefinedBest Practices
响应缓存
Schema Design
—
- Use semantic field names (, not
createdAt)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) }undefinedError 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:
- Define schema for core entities
- Write resolvers with DataLoader
- Add authentication/authorization
- Implement error handling
- Optimize with caching
- Add subscriptions if needed
- 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
—
| 特性 | GraphQL | REST |
|---|---|---|
| 端点 | 单个端点 | 多个端点 |
| 数据获取 | 客户端指定字段 | 服务器决定响应内容 |
| 过度获取 | 无 | 有(返回额外字段) |
| 获取不足 | 无 | 有(需要多次请求) |
| 版本管理 | 不需要 | 必须(v1、v2) |
| 缓存 | 复杂 | 简单(HTTP缓存) |
| 类型系统 | 内置 | 外部(OpenAPI) |
| 实时能力 | 订阅 | SSE/WebSocket |
—
GraphQL vs tRPC
—
| 特性 | GraphQL | tRPC |
|---|---|---|
| 类型安全 | 需要代码生成 | 原生TypeScript支持 |
| 语言支持 | 任意语言 | 仅TypeScript |
| 客户端-服务端耦合 | 松散 | 紧密 |
| Schema | 需要SDL | 从代码中推断 |
| 学习曲线 | 陡峭 | 平缓 |
| 工具链 | 丰富 | 正在发展 |
| 适用场景 | 公共API、移动应用 | 全栈TypeScript项目 |
—
迁移策略
—
REST到GraphQL(渐进式)
—
typescript
undefined—
1. 包装现有REST端点
—
const resolvers = {
Query: {
user: async (_, { id }) => {
const response = await fetch();
return response.json();
},
},
};
https://api.example.com/users/${id}—
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)
入门建议:
- 为核心实体定义Schema
- 使用DataLoader编写解析器
- 添加认证与授权
- 实现错误处理
- 通过缓存优化性能
- 按需添加订阅功能
- 监控并迭代
当API灵活性和开发者体验是优先考虑因素时,GraphQL表现出色。结合TypeScript代码生成,它能提供从数据库到UI的端到端类型安全。