graphql-api-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGraphQL API Development
GraphQL API 开发
A comprehensive skill for building production-ready GraphQL APIs using graphql-js. Master schema design, type systems, resolvers, queries, mutations, subscriptions, authentication, authorization, caching, testing, and deployment strategies.
这是一份使用graphql-js构建可用于生产环境的GraphQL API的综合指南。你将掌握Schema设计、类型系统、解析器、查询、变更、订阅、身份验证、授权、缓存、测试以及部署策略。
When to Use This Skill
何时使用本指南
Use this skill when:
- Building a new API that requires flexible data fetching for web or mobile clients
- Replacing or augmenting REST APIs with more efficient data access patterns
- Developing APIs for applications with complex, nested data relationships
- Creating APIs that serve multiple client types (web, mobile, desktop) with different data needs
- Building real-time applications requiring subscriptions and live updates
- Designing APIs where clients need to specify exactly what data they need
- Developing GraphQL servers with Node.js and Express
- Implementing type-safe APIs with strong schema validation
- Creating self-documenting APIs with built-in introspection
- Building microservices that need to be composed into a unified API
在以下场景使用本指南:
- 构建需要为Web或移动客户端提供灵活数据获取能力的新API
- 用更高效的数据访问模式替代或增强REST API
- 开发具有复杂嵌套数据关系的应用API
- 创建为多种客户端类型(Web、移动、桌面)提供服务且数据需求不同的API
- 构建需要订阅和实时更新的实时应用
- 设计允许客户端精确指定所需数据的API
- 使用Node.js和Express开发GraphQL服务器
- 实现具有强Schema验证的类型安全API
- 创建内置自省功能的自文档化API
- 构建需要组合成统一API的微服务
When GraphQL Excels Over REST
GraphQL 优于 REST 的场景
GraphQL Advantages
GraphQL 的优势
- Precise Data Fetching: Clients request exactly what they need, no over/under-fetching
- Single Request: Fetch multiple resources in one roundtrip instead of multiple REST endpoints
- Strongly Typed: Schema defines exact types, enabling validation and tooling
- Introspection: Self-documenting API with queryable schema
- Versioning Not Required: Add new fields without breaking existing queries
- Real-time Updates: Built-in subscription support for live data
- Nested Resources: Naturally handle complex relationships without N+1 queries
- Client-Driven: Clients control data shape, reducing backend changes
- 精准数据获取:客户端仅请求所需数据,避免过度获取或获取不足
- 单一请求:一次往返请求即可获取多个资源,无需调用多个REST端点
- 强类型约束:Schema定义精确类型,支持验证和工具集成
- 自省功能:具备可查询Schema的自文档化API
- 无需版本化:添加新字段不会破坏现有查询
- 实时更新:内置订阅支持以获取实时数据
- 嵌套资源:自然处理复杂关系,避免N+1查询问题
- 客户端驱动:客户端控制数据结构,减少后端变更需求
When to Stick with REST
何时仍选择 REST
- Simple CRUD operations with standard resources
- File uploads/downloads (GraphQL requires multipart handling)
- HTTP caching is critical (GraphQL typically uses POST)
- Team unfamiliar with GraphQL (learning curve)
- Existing REST infrastructure works well
- 具有标准资源的简单CRUD操作
- 文件上传/下载(GraphQL需要多部分处理)
- HTTP缓存至关重要的场景(GraphQL通常使用POST请求)
- 团队不熟悉GraphQL(存在学习曲线)
- 现有REST基础设施运行良好
Core Concepts
核心概念
The GraphQL Type System
GraphQL 类型系统
GraphQL's type system is its foundation. Every GraphQL API defines:
- Scalar Types: Basic data types (String, Int, Float, Boolean, ID)
- Object Types: Complex types with fields
- Query Type: Entry point for read operations
- Mutation Type: Entry point for write operations
- Subscription Type: Entry point for real-time updates
- Input Types: Complex inputs for mutations
- Enums: Fixed set of values
- Interfaces: Abstract types that objects implement
- Unions: Types that can be one of several types
- Non-Null Types: Types that cannot be null
- List Types: Arrays of types
GraphQL的类型系统是其基础。每个GraphQL API都定义了:
- 标量类型:基础数据类型(String、Int、Float、Boolean、ID)
- 对象类型:包含字段的复杂类型
- 查询类型:读取操作的入口点
- 变更类型:写入操作的入口点
- 订阅类型:实时更新的入口点
- 输入类型:用于变更的复杂输入
- 枚举类型:固定值集合
- 接口:对象实现的抽象类型
- 联合类型:可以是多种类型之一的类型
- 非空类型:不能为空的类型
- 列表类型:类型的数组
Schema Definition
Schema 定义
Two approaches for defining GraphQL schemas:
1. Schema Definition Language (SDL) - Declarative, readable:
graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}2. Programmatic API - Type-safe, programmatic:
javascript
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
posts: { type: new GraphQLList(new GraphQLNonNull(PostType)) }
}
});定义GraphQL Schema有两种方式:
1. Schema定义语言(SDL) - 声明式、可读性强:
graphql
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
}
type Post {
id: ID!
title: String!
content: String
author: User!
}
type Query {
user(id: ID!): User
posts: [Post!]!
}2. 程序化API - 类型安全、可编程:
javascript
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: new GraphQLNonNull(GraphQLID) },
name: { type: new GraphQLNonNull(GraphQLString) },
email: { type: new GraphQLNonNull(GraphQLString) },
posts: { type: new GraphQLList(new GraphQLNonNull(PostType)) }
}
});Resolvers
解析器
Resolvers are functions that return data for schema fields. Every field can have a resolver:
javascript
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return context.db.findUserById(args.id);
}
},
User: {
posts: (user, args, context) => {
return context.db.findPostsByAuthorId(user.id);
}
}
};Resolver Function Signature:
- : The result from the parent resolver
parent - : Arguments passed to the field
args - : Shared context (database, auth, etc.)
context - : Field-specific metadata
info
解析器是为Schema字段返回数据的函数。每个字段都可以有对应的解析器:
javascript
const resolvers = {
Query: {
user: (parent, args, context, info) => {
return context.db.findUserById(args.id);
}
},
User: {
posts: (user, args, context) => {
return context.db.findPostsByAuthorId(user.id);
}
}
};解析器函数签名:
- :父解析器的结果
parent - :传递给字段的参数
args - :共享上下文(数据库、认证信息等)
context - :字段特定的元数据
info
Queries
查询
Queries fetch data from your API:
graphql
query GetUser {
user(id: "123") {
id
name
email
posts {
title
content
}
}
}查询用于从API获取数据:
graphql
query GetUser {
user(id: "123") {
id
name
email
posts {
title
content
}
}
}Mutations
变更
Mutations modify data:
graphql
mutation CreatePost {
createPost(input: {
title: "GraphQL is awesome"
content: "Here's why..."
authorId: "123"
}) {
id
title
author {
name
}
}
}变更用于修改数据:
graphql
mutation CreatePost {
createPost(input: {
title: "GraphQL is awesome"
content: "Here's why..."
authorId: "123"
}) {
id
title
author {
name
}
}
}Subscriptions
订阅
Subscriptions enable real-time updates:
graphql
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}订阅支持实时更新:
graphql
subscription OnPostCreated {
postCreated {
id
title
author {
name
}
}
}Schema Design Patterns
Schema 设计模式
Pattern 1: Input Types for Mutations
模式1:变更使用输入类型
Always use input types for complex mutation arguments:
graphql
input CreateUserInput {
name: String!
email: String!
age: Int
bio: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
}Why: Easier to extend, better organization, reusable across mutations.
始终为复杂变更参数使用输入类型:
graphql
input CreateUserInput {
name: String!
email: String!
age: Int
bio: String
}
type Mutation {
createUser(input: CreateUserInput!): User!
}原因:易于扩展、组织性更好、可在多个变更中复用。
Pattern 2: Interfaces for Shared Fields
模式2:共享字段使用接口
Use interfaces when multiple types share fields:
graphql
interface Node {
id: ID!
createdAt: String!
updatedAt: String!
}
type User implements Node {
id: ID!
createdAt: String!
updatedAt: String!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: String!
updatedAt: String!
title: String!
content: String
}当多个类型共享字段时使用接口:
graphql
interface Node {
id: ID!
createdAt: String!
updatedAt: String!
}
type User implements Node {
id: ID!
createdAt: String!
updatedAt: String!
name: String!
email: String!
}
type Post implements Node {
id: ID!
createdAt: String!
updatedAt: String!
title: String!
content: String
}Pattern 3: Unions for Polymorphic Returns
模式3:多态返回使用联合类型
Use unions when a field can return different types:
graphql
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}当字段可以返回不同类型时使用联合类型:
graphql
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}Pattern 4: Pagination Patterns
模式4:分页模式
Offset-based pagination:
graphql
type Query {
posts(offset: Int, limit: Int): PostConnection!
}
type PostConnection {
items: [Post!]!
total: Int!
hasMore: Boolean!
}Cursor-based pagination (Relay-style):
graphql
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}基于偏移量的分页:
graphql
type Query {
posts(offset: Int, limit: Int): PostConnection!
}
type PostConnection {
items: [Post!]!
total: Int!
hasMore: Boolean!
}基于游标分页(Relay风格):
graphql
type Query {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
endCursor: String
}Pattern 5: Error Handling
模式5:错误处理
Field-level errors:
graphql
type MutationPayload {
success: Boolean!
message: String
user: User
errors: [Error!]
}
type Error {
field: String!
message: String!
}Union-based error handling:
graphql
union CreateUserResult = User | ValidationError | DatabaseError
type ValidationError {
field: String!
message: String!
}字段级错误:
graphql
type MutationPayload {
success: Boolean!
message: String
user: User
errors: [Error!]
}
type Error {
field: String!
message: String!
}基于联合类型的错误处理:
graphql
union CreateUserResult = User | ValidationError | DatabaseError
type ValidationError {
field: String!
message: String!
}Pattern 6: Versioning with Directives
模式6:使用指令进行版本控制
Deprecate fields instead of versioning:
graphql
type User {
name: String! @deprecated(reason: "Use firstName and lastName")
firstName: String!
lastName: String!
}通过弃用字段替代版本化:
graphql
type User {
name: String! @deprecated(reason: "Use firstName and lastName")
firstName: String!
lastName: String!
}Query Optimization and Performance
查询优化与性能
The N+1 Problem
N+1问题
Problem: Fetching nested data causes multiple database queries:
javascript
// BAD: N+1 queries
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user) => {
// This runs once PER user!
return db.getPostsByUserId(user.id);
}
}
}
});
// Query for 100 users = 1 query for users + 100 queries for posts = 101 queries问题:获取嵌套数据会导致多次数据库查询:
javascript
// BAD: N+1 queries
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user) => {
// This runs once PER user!
return db.getPostsByUserId(user.id);
}
}
}
});
// Query for 100 users = 1 query for users + 100 queries for posts = 101 queriesDataLoader Solution
DataLoader解决方案
DataLoader batches and caches requests:
javascript
import DataLoader from 'dataloader';
// Create DataLoader
const postLoader = new DataLoader(async (userIds) => {
// Single query for all user IDs
const posts = await db.getPostsByUserIds(userIds);
// Group posts by userId
const postsByUserId = {};
posts.forEach(post => {
if (!postsByUserId[post.authorId]) {
postsByUserId[post.authorId] = [];
}
postsByUserId[post.authorId].push(post);
});
// Return in same order as userIds
return userIds.map(id => postsByUserId[id] || []);
});
// Use in resolver
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
}
});
// Add to context
const context = {
loaders: {
postLoader: new DataLoader(batchLoadPosts)
}
};DataLoader用于批量处理和缓存请求:
javascript
import DataLoader from 'dataloader';
// Create DataLoader
const postLoader = new DataLoader(async (userIds) => {
// Single query for all user IDs
const posts = await db.getPostsByUserIds(userIds);
// Group posts by userId
const postsByUserId = {};
posts.forEach(post => {
if (!postsByUserId[post.authorId]) {
postsByUserId[post.authorId] = [];
}
postsByUserId[post.authorId].push(post);
});
// Return in same order as userIds
return userIds.map(id => postsByUserId[id] || []);
});
// Use in resolver
const UserType = new GraphQLObjectType({
name: 'User',
fields: {
posts: {
type: new GraphQLList(PostType),
resolve: (user, args, context) => {
return context.loaders.postLoader.load(user.id);
}
}
}
});
// Add to context
const context = {
loaders: {
postLoader: new DataLoader(batchLoadPosts)
}
};Query Complexity Analysis
查询复杂度分析
Limit expensive queries:
javascript
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
const complexity = getComplexity({
schema,
query,
estimators: [
simpleEstimator({ defaultComplexity: 1 })
]
});
if (complexity > 1000) {
throw new Error('Query too complex');
}限制资源密集型查询:
javascript
import { getComplexity, simpleEstimator } from 'graphql-query-complexity';
const complexity = getComplexity({
schema,
query,
estimators: [
simpleEstimator({ defaultComplexity: 1 })
]
});
if (complexity > 1000) {
throw new Error('Query too complex');
}Depth Limiting
深度限制
Prevent deeply nested queries:
javascript
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(5)]
});防止深度嵌套查询:
javascript
import depthLimit from 'graphql-depth-limit';
const server = new ApolloServer({
schema,
validationRules: [depthLimit(5)]
});Mutations and Input Validation
变更与输入验证
Mutation Design Pattern
变更设计模式
graphql
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
type CreatePostPayload {
post: Post
errors: [UserError!]
success: Boolean!
}
type UserError {
message: String!
field: String
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}graphql
input CreatePostInput {
title: String!
content: String!
authorId: ID!
tags: [String!]
}
type CreatePostPayload {
post: Post
errors: [UserError!]
success: Boolean!
}
type UserError {
message: String!
field: String
}
type Mutation {
createPost(input: CreatePostInput!): CreatePostPayload!
}Input Validation
输入验证
javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: CreatePostPayload,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
// Validate input
const errors = [];
if (input.title.length < 3) {
errors.push({
field: 'title',
message: 'Title must be at least 3 characters'
});
}
if (input.content.length < 10) {
errors.push({
field: 'content',
message: 'Content must be at least 10 characters'
});
}
if (errors.length > 0) {
return { errors, success: false, post: null };
}
// Create post
const post = await context.db.createPost(input);
return { post, errors: [], success: true };
}
}
}
});javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: CreatePostPayload,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
// Validate input
const errors = [];
if (input.title.length < 3) {
errors.push({
field: 'title',
message: 'Title must be at least 3 characters'
});
}
if (input.content.length < 10) {
errors.push({
field: 'content',
message: 'Content must be at least 10 characters'
});
}
if (errors.length > 0) {
return { errors, success: false, post: null };
}
// Create post
const post = await context.db.createPost(input);
return { post, errors: [], success: true };
}
}
}
});Subscriptions and Real-time Updates
订阅与实时更新
Setting Up Subscriptions
设置订阅
javascript
import { GraphQLObjectType, GraphQLString } from 'graphql';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const Subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
postCreated: {
type: PostType,
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
messageReceived: {
type: MessageType,
args: {
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
}
}
}
});javascript
import { GraphQLObjectType, GraphQLString } from 'graphql';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
const Subscription = new GraphQLObjectType({
name: 'Subscription',
fields: {
postCreated: {
type: PostType,
subscribe: () => pubsub.asyncIterator(['POST_CREATED'])
},
messageReceived: {
type: MessageType,
args: {
channelId: { type: new GraphQLNonNull(GraphQLID) }
},
subscribe: (_, { channelId }) => {
return pubsub.asyncIterator([`MESSAGE_${channelId}`]);
}
}
}
});Publishing Events
发布事件
javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: PostType,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
const post = await context.db.createPost(input);
// Publish to subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
}
});javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
createPost: {
type: PostType,
args: {
input: { type: new GraphQLNonNull(CreatePostInput) }
},
resolve: async (_, { input }, context) => {
const post = await context.db.createPost(input);
// Publish to subscribers
pubsub.publish('POST_CREATED', { postCreated: post });
return post;
}
}
}
});WebSocket Server Setup
WebSocket服务器设置
javascript
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import express from 'express';
const app = express();
const httpServer = createServer(app);
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
useServer(
{
schema,
execute,
subscribe,
context: (ctx) => {
// Access connection params, headers
return {
userId: ctx.connectionParams?.userId,
db: database
};
}
},
wsServer
);
httpServer.listen(4000);javascript
import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { execute, subscribe } from 'graphql';
import express from 'express';
const app = express();
const httpServer = createServer(app);
// WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql'
});
useServer(
{
schema,
execute,
subscribe,
context: (ctx) => {
// Access connection params, headers
return {
userId: ctx.connectionParams?.userId,
db: database
};
}
},
wsServer
);
httpServer.listen(4000);Authentication and Authorization
身份验证与授权
Context-Based Authentication
基于上下文的身份验证
javascript
import jwt from 'jsonwebtoken';
// Middleware to extract user
const authMiddleware = async (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.findUserById(decoded.userId);
return { user };
} catch (error) {
return { user: null };
}
};
// Add to GraphQL context
app.all('/graphql', async (req, res) => {
const auth = await authMiddleware(req);
createHandler({
schema,
context: {
user: auth.user,
db: database
}
})(req, res);
});javascript
import jwt from 'jsonwebtoken';
// Middleware to extract user
const authMiddleware = async (req) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return { user: null };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await db.findUserById(decoded.userId);
return { user };
} catch (error) {
return { user: null };
}
};
// Add to GraphQL context
app.all('/graphql', async (req, res) => {
const auth = await authMiddleware(req);
createHandler({
schema,
context: {
user: auth.user,
db: database
}
})(req, res);
});Resolver-Level Authorization
解析器级授权
javascript
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
me: {
type: UserType,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
return context.user;
}
},
adminData: {
type: GraphQLString,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
if (context.user.role !== 'admin') {
throw new Error('Admin access required');
}
return 'Secret admin data';
}
}
}
});javascript
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
me: {
type: UserType,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
return context.user;
}
},
adminData: {
type: GraphQLString,
resolve: (_, __, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
if (context.user.role !== 'admin') {
throw new Error('Admin access required');
}
return 'Secret admin data';
}
}
}
});Field-Level Authorization
字段级授权
javascript
const PostType = new GraphQLObjectType({
name: 'Post',
fields: {
title: { type: GraphQLString },
content: { type: GraphQLString },
draft: {
type: GraphQLBoolean,
resolve: (post, args, context) => {
// Only author can see draft status
if (post.authorId !== context.user?.id) {
return null;
}
return post.draft;
}
}
}
});javascript
const PostType = new GraphQLObjectType({
name: 'Post',
fields: {
title: { type: GraphQLString },
content: { type: GraphQLString },
draft: {
type: GraphQLBoolean,
resolve: (post, args, context) => {
// Only author can see draft status
if (post.authorId !== context.user?.id) {
return null;
}
return post.draft;
}
}
}
});Directive-Based Authorization
基于指令的授权
graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
type Query {
publicData: String
userData: String @auth(requires: USER)
adminData: String @auth(requires: ADMIN)
}javascript
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
function authDirective(schema, directiveName) {
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.user) {
throw new Error('Authentication required');
}
if (context.user.role !== requires) {
throw new Error(`${requires} role required`);
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
}
});
}graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION
enum Role {
USER
ADMIN
MODERATOR
}
type Query {
publicData: String
userData: String @auth(requires: USER)
adminData: String @auth(requires: ADMIN)
}javascript
import { mapSchema, getDirective, MapperKind } from '@graphql-tools/utils';
function authDirective(schema, directiveName) {
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.user) {
throw new Error('Authentication required');
}
if (context.user.role !== requires) {
throw new Error(`${requires} role required`);
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
}
});
}Caching Strategies
缓存策略
In-Memory Caching
内存缓存
javascript
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5 // 5 minutes
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
product: {
type: ProductType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `product:${id}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const product = await context.db.findProductById(id);
cache.set(cacheKey, product);
return product;
}
}
}
});javascript
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({
max: 500,
ttl: 1000 * 60 * 5 // 5 minutes
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
product: {
type: ProductType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `product:${id}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const product = await context.db.findProductById(id);
cache.set(cacheKey, product);
return product;
}
}
}
});Redis Caching
Redis缓存
javascript
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `user:${id}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await context.db.findUserById(id);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(user));
return user;
}
}
}
});javascript
import Redis from 'ioredis';
const redis = new Redis({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
const cacheKey = `user:${id}`;
// Check cache
const cached = await redis.get(cacheKey);
if (cached) {
return JSON.parse(cached);
}
// Fetch from database
const user = await context.db.findUserById(id);
// Cache for 10 minutes
await redis.setex(cacheKey, 600, JSON.stringify(user));
return user;
}
}
}
});Cache Invalidation
缓存失效
javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
input: { type: new GraphQLNonNull(UpdateUserInput) }
},
resolve: async (_, { id, input }, context) => {
const user = await context.db.updateUser(id, input);
// Invalidate cache
const cacheKey = `user:${id}`;
await redis.del(cacheKey);
// Also invalidate list caches
await redis.del('users:all');
return user;
}
}
}
});javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
updateUser: {
type: UserType,
args: {
id: { type: new GraphQLNonNull(GraphQLID) },
input: { type: new GraphQLNonNull(UpdateUserInput) }
},
resolve: async (_, { id, input }, context) => {
const user = await context.db.updateUser(id, input);
// Invalidate cache
const cacheKey = `user:${id}`;
await redis.del(cacheKey);
// Also invalidate list caches
await redis.del('users:all');
return user;
}
}
}
});Error Handling
错误处理
Custom Error Classes
自定义错误类
javascript
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
this.extensions = { code: 'UNAUTHENTICATED' };
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.name = 'ForbiddenError';
this.extensions = { code: 'FORBIDDEN' };
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.extensions = {
code: 'BAD_USER_INPUT',
fields
};
}
}javascript
class AuthenticationError extends Error {
constructor(message) {
super(message);
this.name = 'AuthenticationError';
this.extensions = { code: 'UNAUTHENTICATED' };
}
}
class ForbiddenError extends Error {
constructor(message) {
super(message);
this.name = 'ForbiddenError';
this.extensions = { code: 'FORBIDDEN' };
}
}
class ValidationError extends Error {
constructor(message, fields) {
super(message);
this.name = 'ValidationError';
this.extensions = {
code: 'BAD_USER_INPUT',
fields
};
}
}Error Formatting
错误格式化
javascript
import { formatError } from 'graphql';
const customFormatError = (error) => {
// Log error for monitoring
console.error('GraphQL Error:', {
message: error.message,
locations: error.locations,
path: error.path,
extensions: error.extensions
});
// Don't expose internal errors to clients
if (error.message.startsWith('Database')) {
return {
message: 'Internal server error',
extensions: { code: 'INTERNAL_SERVER_ERROR' }
};
}
return formatError(error);
};
const server = new ApolloServer({
schema,
formatError: customFormatError
});javascript
import { formatError } from 'graphql';
const customFormatError = (error) => {
// Log error for monitoring
console.error('GraphQL Error:', {
message: error.message,
locations: error.locations,
path: error.path,
extensions: error.extensions
});
// Don't expose internal errors to clients
if (error.message.startsWith('Database')) {
return {
message: 'Internal server error',
extensions: { code: 'INTERNAL_SERVER_ERROR' }
};
}
return formatError(error);
};
const server = new ApolloServer({
schema,
formatError: customFormatError
});Graceful Error Responses
优雅的错误响应
javascript
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
try {
const user = await context.db.findUserById(id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
return user;
} catch (error) {
// Log error
console.error('Error fetching user:', error);
// Re-throw with user-friendly message
if (error.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to database');
}
throw error;
}
}
}
}
});javascript
const Query = new GraphQLObjectType({
name: 'Query',
fields: {
user: {
type: UserType,
args: { id: { type: new GraphQLNonNull(GraphQLID) } },
resolve: async (_, { id }, context) => {
try {
const user = await context.db.findUserById(id);
if (!user) {
throw new Error(`User with ID ${id} not found`);
}
return user;
} catch (error) {
// Log error
console.error('Error fetching user:', error);
// Re-throw with user-friendly message
if (error.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to database');
}
throw error;
}
}
}
}
});Testing GraphQL APIs
测试GraphQL API
Unit Testing Resolvers
解析器单元测试
javascript
import { describe, it, expect, jest } from '@jest/globals';
describe('User resolver', () => {
it('returns user by ID', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue({
id: '1',
name: 'Alice',
email: 'alice@example.com'
})
};
const context = { db: mockDb };
const result = await userResolver.resolve(null, { id: '1' }, context);
expect(mockDb.findUserById).toHaveBeenCalledWith('1');
expect(result).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com'
});
});
it('throws error for non-existent user', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue(null)
};
const context = { db: mockDb };
await expect(
userResolver.resolve(null, { id: '999' }, context)
).rejects.toThrow('User with ID 999 not found');
});
});javascript
import { describe, it, expect, jest } from '@jest/globals';
describe('User resolver', () => {
it('returns user by ID', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue({
id: '1',
name: 'Alice',
email: 'alice@example.com'
})
};
const context = { db: mockDb };
const result = await userResolver.resolve(null, { id: '1' }, context);
expect(mockDb.findUserById).toHaveBeenCalledWith('1');
expect(result).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com'
});
});
it('throws error for non-existent user', async () => {
const mockDb = {
findUserById: jest.fn().mockResolvedValue(null)
};
const context = { db: mockDb };
await expect(
userResolver.resolve(null, { id: '999' }, context)
).rejects.toThrow('User with ID 999 not found');
});
});Integration Testing
集成测试
javascript
import { graphql } from 'graphql';
import { schema } from './schema';
describe('GraphQL Schema', () => {
it('executes user query', async () => {
const query = `
query {
user(id: "1") {
id
name
email
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeUndefined();
expect(result.data?.user).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com'
});
});
it('handles authentication errors', async () => {
const query = `
query {
me {
id
name
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toBe('Authentication required');
});
});javascript
import { graphql } from 'graphql';
import { schema } from './schema';
describe('GraphQL Schema', () => {
it('executes user query', async () => {
const query = `
query {
user(id: "1") {
id
name
email
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeUndefined();
expect(result.data?.user).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com'
});
});
it('handles authentication errors', async () => {
const query = `
query {
me {
id
name
}
}
`;
const result = await graphql({
schema,
source: query,
contextValue: {
db: mockDatabase,
user: null
}
});
expect(result.errors).toBeDefined();
expect(result.errors[0].message).toBe('Authentication required');
});
});Testing with Apollo Server
使用Apollo Server测试
javascript
import { ApolloServer } from '@apollo/server';
const testServer = new ApolloServer({
schema,
});
describe('User queries', () => {
it('fetches user successfully', async () => {
const response = await testServer.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id: '1' }
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toMatchObject({
id: '1',
name: expect.any(String)
});
});
});javascript
import { ApolloServer } from '@apollo/server';
const testServer = new ApolloServer({
schema,
});
describe('User queries', () => {
it('fetches user successfully', async () => {
const response = await testServer.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
}
}
`,
variables: { id: '1' }
});
expect(response.body.singleResult.errors).toBeUndefined();
expect(response.body.singleResult.data?.user).toMatchObject({
id: '1',
name: expect.any(String)
});
});
});Production Best Practices
生产环境最佳实践
Schema Organization
Schema 组织
src/
├── schema/
│ ├── index.js # Combine all types
│ ├── types/
│ │ ├── user.js # User type and resolvers
│ │ ├── post.js # Post type and resolvers
│ │ └── comment.js # Comment type and resolvers
│ ├── queries/
│ │ ├── user.js # User queries
│ │ └── post.js # Post queries
│ ├── mutations/
│ │ ├── user.js # User mutations
│ │ └── post.js # Post mutations
│ └── subscriptions/
│ └── post.js # Post subscriptions
├── directives/
│ └── auth.js # Authorization directive
├── utils/
│ ├── loaders.js # DataLoader instances
│ └── context.js # Context builder
└── server.js # Server setupsrc/
├── schema/
│ ├── index.js # 合并所有类型
│ ├── types/
│ │ ├── user.js # User类型与解析器
│ │ ├── post.js # Post类型与解析器
│ │ └── comment.js # Comment类型与解析器
│ ├── queries/
│ │ ├── user.js # User查询
│ │ └── post.js # Post查询
│ ├── mutations/
│ │ ├── user.js # User变更
│ │ └── post.js # Post变更
│ └── subscriptions/
│ └── post.js # Post订阅
├── directives/
│ └── auth.js # 授权指令
├── utils/
│ ├── loaders.js # DataLoader实例
│ └── context.js # 上下文构建器
└── server.js # 服务器设置Monitoring and Logging
监控与日志
javascript
import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground';
const server = new ApolloServer({
schema,
plugins: [
// Request logging
{
async requestDidStart(requestContext) {
console.log('Request started:', requestContext.request.query);
return {
async didEncounterErrors(ctx) {
console.error('Errors:', ctx.errors);
},
async willSendResponse(ctx) {
console.log('Response sent');
}
};
}
},
// Performance monitoring
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse() {
const duration = Date.now() - start;
console.log(`Request duration: ${duration}ms`);
}
};
}
}
]
});javascript
import { ApolloServerPluginLandingPageGraphQLPlayground } from '@apollo/server-plugin-landing-page-graphql-playground';
const server = new ApolloServer({
schema,
plugins: [
// 请求日志
{
async requestDidStart(requestContext) {
console.log('Request started:', requestContext.request.query);
return {
async didEncounterErrors(ctx) {
console.error('Errors:', ctx.errors);
},
async willSendResponse(ctx) {
console.log('Response sent');
}
};
}
},
// 性能监控
{
async requestDidStart() {
const start = Date.now();
return {
async willSendResponse() {
const duration = Date.now() - start;
console.log(`Request duration: ${duration}ms`);
}
};
}
}
]
});Rate Limiting
请求频率限制
javascript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests, please try again later'
});
app.use('/graphql', limiter);javascript
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP在窗口时间内最多100次请求
message: '请求过于频繁,请稍后再试'
});
app.use('/graphql', limiter);Query Whitelisting
查询白名单
javascript
const allowedQueries = new Set([
'query GetUser { user(id: $id) { id name email } }',
'mutation CreatePost { createPost(input: $input) { id title } }'
]);
const validateQuery = (query) => {
const normalized = query.replace(/\s+/g, ' ').trim();
if (!allowedQueries.has(normalized)) {
throw new Error('Query not whitelisted');
}
};javascript
const allowedQueries = new Set([
'query GetUser { user(id: $id) { id name email } }',
'mutation CreatePost { createPost(input: $input) { id title } }'
]);
const validateQuery = (query) => {
const normalized = query.replace(/\s+/g, ' ').trim();
if (!allowedQueries.has(normalized)) {
throw new Error('Query not whitelisted');
}
};Security Headers
安全头
javascript
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
}
},
crossOriginEmbedderPolicy: false
}));javascript
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
}
},
crossOriginEmbedderPolicy: false
}));Advanced Patterns
高级模式
Federation (Microservices)
联邦(微服务)
javascript
import { buildSubgraphSchema } from '@apollo/subgraph';
// Users service
const userSchema = buildSubgraphSchema({
typeDefs: `
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`,
resolvers: {
User: {
__resolveReference(user) {
return findUserById(user.id);
}
}
}
});
// Posts service
const postSchema = buildSubgraphSchema({
typeDefs: `
type Post {
id: ID!
title: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
`,
resolvers: {
Post: {
author(post) {
return { __typename: 'User', id: post.authorId };
}
},
User: {
posts(user) {
return findPostsByAuthorId(user.id);
}
}
}
});javascript
import { buildSubgraphSchema } from '@apollo/subgraph';
// 用户服务
const userSchema = buildSubgraphSchema({
typeDefs: `
type User @key(fields: "id") {
id: ID!
name: String!
email: String!
}
`,
resolvers: {
User: {
__resolveReference(user) {
return findUserById(user.id);
}
}
}
});
// 帖子服务
const postSchema = buildSubgraphSchema({
typeDefs: `
type Post {
id: ID!
title: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
`,
resolvers: {
Post: {
author(post) {
return { __typename: 'User', id: post.authorId };
}
},
User: {
posts(user) {
return findPostsByAuthorId(user.id);
}
}
}
});Custom Scalars
自定义标量
javascript
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 DateTime string',
serialize(value) {
// Send to client
return value instanceof Date ? value.toISOString() : null;
},
parseValue(value) {
// From variables
return new Date(value);
},
parseLiteral(ast) {
// From query string
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
}
});
// Use in schema
const schema = new GraphQLSchema({
types: [DateTimeScalar],
query: new GraphQLObjectType({
name: 'Query',
fields: {
now: {
type: DateTimeScalar,
resolve: () => new Date()
}
}
})
});javascript
import { GraphQLScalarType, Kind } from 'graphql';
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO-8601 DateTime string',
serialize(value) {
// Send to client
return value instanceof Date ? value.toISOString() : null;
},
parseValue(value) {
// From variables
return new Date(value);
},
parseLiteral(ast) {
// From query string
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
return null;
}
});
// Use in schema
const schema = new GraphQLSchema({
types: [DateTimeScalar],
query: new GraphQLObjectType({
name: 'Query',
fields: {
now: {
type: DateTimeScalar,
resolve: () => new Date()
}
}
})
});Batch Operations
批量操作
javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
batchCreateUsers: {
type: new GraphQLList(UserType),
args: {
inputs: {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(CreateUserInput))
)
}
},
resolve: async (_, { inputs }, context) => {
const users = await Promise.all(
inputs.map(input => context.db.createUser(input))
);
return users;
}
}
}
});javascript
const Mutation = new GraphQLObjectType({
name: 'Mutation',
fields: {
batchCreateUsers: {
type: new GraphQLList(UserType),
args: {
inputs: {
type: new GraphQLNonNull(
new GraphQLList(new GraphQLNonNull(CreateUserInput))
)
}
},
resolve: async (_, { inputs }, context) => {
const users = await Promise.all(
inputs.map(input => context.db.createUser(input))
);
return users;
}
}
}
});Common Patterns Summary
常见模式总结
- Use Input Types: For all mutations with multiple arguments
- Implement DataLoader: Solve N+1 queries for nested data
- Add Pagination: For list fields that can grow unbounded
- Handle Errors Gracefully: Return user-friendly error messages
- Validate Inputs: At resolver level before database operations
- Use Context for Shared State: Database, authentication, loaders
- Implement Authorization: At resolver or directive level
- Cache Aggressively: Use Redis or in-memory for frequently accessed data
- Monitor Performance: Track query complexity and execution time
- Version with @deprecated: Never break existing queries
- Test Thoroughly: Unit test resolvers, integration test queries
- Document Schema: Use descriptions in SDL
- Use Non-Null Wisely: Only for truly required fields
- Organize Schema: Split into modules by domain
- Secure Production: Rate limiting, query whitelisting, depth limiting
- 使用输入类型:所有包含多个参数的变更都使用输入类型
- 实现DataLoader:解决嵌套数据的N+1查询问题
- 添加分页:针对可能无限增长的列表字段
- 优雅处理错误:返回用户友好的错误信息
- 验证输入:在数据库操作前的解析器层进行验证
- 使用上下文存储共享状态:数据库、认证信息、加载器等
- 实现授权:在解析器或指令层进行授权
- 积极缓存:对频繁访问的数据使用Redis或内存缓存
- 监控性能:跟踪查询复杂度和执行时间
- 使用@deprecated进行版本控制:永远不要破坏现有查询
- 全面测试:单元测试解析器,集成测试查询
- 文档化Schema:在SDL中使用描述信息
- 合理使用非空类型:仅用于真正必填的字段
- 组织Schema:按领域拆分为模块
- 生产环境安全:请求频率限制、查询白名单、深度限制
Resources and Tools
资源与工具
Essential Libraries
核心库
- graphql-js: Core GraphQL implementation
- express: Web server framework
- graphql-http: HTTP handler for GraphQL
- dataloader: Batching and caching
- graphql-ws: WebSocket server for subscriptions
- graphql-scalars: Common custom scalars
- graphql-tools: Schema manipulation utilities
- graphql-js:GraphQL核心实现
- express:Web服务器框架
- graphql-http:GraphQL的HTTP处理器
- dataloader:批量处理与缓存
- graphql-ws:用于订阅的WebSocket服务器
- graphql-scalars:常用自定义标量
- graphql-tools:Schema操作工具
Development Tools
开发工具
- GraphiQL: In-browser GraphQL IDE
- GraphQL Playground: Advanced GraphQL IDE
- Apollo Studio: Schema registry and monitoring
- GraphQL Code Generator: Generate TypeScript types
- eslint-plugin-graphql: Lint GraphQL queries
- GraphiQL:浏览器端GraphQL IDE
- GraphQL Playground:高级GraphQL IDE
- Apollo Studio:Schema注册与监控
- GraphQL Code Generator:生成TypeScript类型
- eslint-plugin-graphql:GraphQL查询代码检查
Learning Resources
学习资源
- GraphQL Official Documentation: https://graphql.org
- GraphQL.js Repository: https://github.com/graphql/graphql-js
- How to GraphQL: https://howtographql.com
- Apollo GraphQL: https://apollographql.com
- GraphQL Weekly Newsletter: https://graphqlweekly.com
Skill Version: 1.0.0
Last Updated: October 2025
Skill Category: API Development, Backend, GraphQL, Web Development
Compatible With: Node.js, Express, TypeScript, JavaScript
- GraphQL官方文档:https://graphql.org
- GraphQL.js仓库:https://github.com/graphql/graphql-js
- How to GraphQL:https://howtographql.com
- Apollo GraphQL:https://apollographql.com
- GraphQL周刊通讯:https://graphqlweekly.com
指南版本:1.0.0
最后更新:2025年10月
指南分类:API开发、后端开发、GraphQL、Web开发
兼容环境:Node.js、Express、TypeScript、JavaScript