graphql-expert
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGraphQL Expert
GraphQL 专家指南
Expert guidance for GraphQL API development, schema design, resolvers, subscriptions, and best practices for building type-safe, efficient APIs.
为GraphQL API开发、Schema设计、解析器、订阅功能提供专业指导,以及构建类型安全、高效API的最佳实践。
Core Concepts
核心概念
Schema Design
Schema设计
- Type system and schema definition language (SDL)
- Object types, interfaces, unions, and enums
- Input types and custom scalars
- Schema stitching and federation
- Modular schema organization
- 类型系统与Schema定义语言(SDL)
- 对象类型、接口、联合类型与枚举类型
- 输入类型与自定义标量
- Schema拼接与联邦
- 模块化Schema组织
Resolvers
解析器
- Resolver functions and data sources
- Context and info arguments
- Field-level resolvers
- Resolver chains and data loaders
- Error handling in resolvers
- 解析器函数与数据源
- Context与info参数
- 字段级解析器
- 解析器链与DataLoader
- 解析器中的错误处理
Queries and Mutations
查询与变更
- Query design and naming conventions
- Mutation patterns and best practices
- Input validation and sanitization
- Pagination strategies (cursor-based, offset)
- Filtering and sorting
- 查询设计与命名规范
- 变更模式与最佳实践
- 输入验证与清理
- 分页策略(基于游标、偏移量)
- 过滤与排序
Subscriptions
订阅
- Real-time updates with WebSocket
- Subscription resolvers
- PubSub patterns
- Subscription filtering
- Connection management
- 基于WebSocket的实时更新
- 订阅解析器
- PubSub模式
- 订阅过滤
- 连接管理
Performance
性能
- N+1 query problem and DataLoader
- Query complexity analysis
- Depth limiting and query cost
- Caching strategies (field-level, full response)
- Batching and deduplication
- N+1查询问题与DataLoader
- 查询复杂度分析
- 深度限制与查询成本
- 缓存策略(字段级、全响应)
- 批处理与去重
Modern GraphQL Development
现代GraphQL开发
Apollo Server 4
Apollo Server 4
typescript
// Apollo Server 4 setup
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
// Type definitions
const typeDefs = `#graphql
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
tags: [String!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
users(limit: Int = 10, offset: Int = 0): UsersConnection!
user(id: ID!): User
posts(filter: PostFilter, sort: SortOrder): [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
publishPost(id: ID!): Post!
}
type Subscription {
postPublished: Post!
userCreated: User!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
email: String
name: String
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
input PostFilter {
published: Boolean
authorId: ID
tag: String
}
type UsersConnection {
nodes: [User!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
enum SortOrder {
NEWEST_FIRST
OLDEST_FIRST
TITLE_ASC
TITLE_DESC
}
scalar DateTime
`;
// Resolvers
const resolvers = {
Query: {
users: async (_, { limit, offset }, { dataSources }) => {
const users = await dataSources.userAPI.getUsers({ limit, offset });
const totalCount = await dataSources.userAPI.getTotalCount();
return {
nodes: users,
totalCount,
pageInfo: {
hasNextPage: offset + limit < totalCount,
hasPreviousPage: offset > 0,
},
};
},
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
posts: async (_, { filter, sort }, { dataSources }) => {
return dataSources.postAPI.getPosts({ filter, sort });
},
post: async (_, { id }, { dataSources }) => {
return dataSources.postAPI.getPostById(id);
},
},
Mutation: {
createUser: async (_, { input }, { dataSources, user }) => {
// Validate input
if (!isValidEmail(input.email)) {
throw new GraphQLError('Invalid email address', {
extensions: { code: 'BAD_USER_INPUT' },
});
}
return dataSources.userAPI.createUser(input);
},
updateUser: async (_, { id, input }, { dataSources, user }) => {
// Check authorization
if (user.id !== id && !user.isAdmin) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return dataSources.userAPI.updateUser(id, input);
},
createPost: async (_, { input }, { dataSources, user, pubsub }) => {
if (!user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const post = await dataSources.postAPI.createPost({
...input,
authorId: user.id,
});
return post;
},
publishPost: async (_, { id }, { dataSources, user, pubsub }) => {
const post = await dataSources.postAPI.publishPost(id);
// Trigger subscription
pubsub.publish('POST_PUBLISHED', { postPublished: post });
return post;
},
},
Subscription: {
postPublished: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['POST_PUBLISHED']),
},
userCreated: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['USER_CREATED']),
},
},
User: {
posts: async (parent, _, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthorId(parent.id);
},
},
Post: {
author: async (parent, _, { dataSources }) => {
return dataSources.userAPI.getUserById(parent.authorId);
},
},
DateTime: new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time string',
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;
},
}),
};
// Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
],
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization || '';
const user = await getUserFromToken(token);
return {
user,
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI(),
},
pubsub,
};
},
listen: { port: 4000 },
});typescript
// Apollo Server 4 setup
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
// Type definitions
const typeDefs = `#graphql
type User {
id: ID!
email: String!
name: String!
posts: [Post!]!
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
published: Boolean!
tags: [String!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Query {
users(limit: Int = 10, offset: Int = 0): UsersConnection!
user(id: ID!): User
posts(filter: PostFilter, sort: SortOrder): [Post!]!
post(id: ID!): Post
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
publishPost(id: ID!): Post!
}
type Subscription {
postPublished: Post!
userCreated: User!
}
input CreateUserInput {
email: String!
name: String!
password: String!
}
input UpdateUserInput {
email: String
name: String
}
input CreatePostInput {
title: String!
content: String!
tags: [String!]
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
}
input PostFilter {
published: Boolean
authorId: ID
tag: String
}
type UsersConnection {
nodes: [User!]!
totalCount: Int!
pageInfo: PageInfo!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}
enum SortOrder {
NEWEST_FIRST
OLDEST_FIRST
TITLE_ASC
TITLE_DESC
}
scalar DateTime
`;
// Resolvers
const resolvers = {
Query: {
users: async (_, { limit, offset }, { dataSources }) => {
const users = await dataSources.userAPI.getUsers({ limit, offset });
const totalCount = await dataSources.userAPI.getTotalCount();
return {
nodes: users,
totalCount,
pageInfo: {
hasNextPage: offset + limit < totalCount,
hasPreviousPage: offset > 0,
},
};
},
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
posts: async (_, { filter, sort }, { dataSources }) => {
return dataSources.postAPI.getPosts({ filter, sort });
},
post: async (_, { id }, { dataSources }) => {
return dataSources.postAPI.getPostById(id);
},
},
Mutation: {
createUser: async (_, { input }, { dataSources, user }) => {
// Validate input
if (!isValidEmail(input.email)) {
throw new GraphQLError('Invalid email address', {
extensions: { code: 'BAD_USER_INPUT' },
});
}
return dataSources.userAPI.createUser(input);
},
updateUser: async (_, { id, input }, { dataSources, user }) => {
// Check authorization
if (user.id !== id && !user.isAdmin) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return dataSources.userAPI.updateUser(id, input);
},
createPost: async (_, { input }, { dataSources, user, pubsub }) => {
if (!user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
const post = await dataSources.postAPI.createPost({
...input,
authorId: user.id,
});
return post;
},
publishPost: async (_, { id }, { dataSources, user, pubsub }) => {
const post = await dataSources.postAPI.publishPost(id);
// Trigger subscription
pubsub.publish('POST_PUBLISHED', { postPublished: post });
return post;
},
},
Subscription: {
postPublished: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['POST_PUBLISHED']),
},
userCreated: {
subscribe: (_, __, { pubsub }) => pubsub.asyncIterator(['USER_CREATED']),
},
},
User: {
posts: async (parent, _, { dataSources }) => {
return dataSources.postAPI.getPostsByAuthorId(parent.id);
},
},
Post: {
author: async (parent, _, { dataSources }) => {
return dataSources.userAPI.getUserById(parent.authorId);
},
},
DateTime: new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601 date-time string',
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;
},
}),
};
// Server setup
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
],
});
const { url } = await startStandaloneServer(server, {
context: async ({ req }) => {
const token = req.headers.authorization || '';
const user = await getUserFromToken(token);
return {
user,
dataSources: {
userAPI: new UserAPI(),
postAPI: new PostAPI(),
},
pubsub,
};
},
listen: { port: 4000 },
});DataLoader for N+1 Prevention
用DataLoader解决N+1查询问题
typescript
import DataLoader from 'dataloader';
// Create DataLoaders
class UserAPI {
private loader: DataLoader<string, User>;
constructor() {
this.loader = new DataLoader(async (ids: readonly string[]) => {
// Batch fetch users
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
// Return in same order as input ids
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) ?? null);
});
}
async getUserById(id: string): Promise<User | null> {
return this.loader.load(id);
}
async getUsersByIds(ids: string[]): Promise<(User | null)[]> {
return this.loader.loadMany(ids);
}
}
// Usage in resolvers
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// This will be batched with DataLoader
return dataSources.userAPI.getUserById(parent.authorId);
},
},
};typescript
import DataLoader from 'dataloader';
// Create DataLoaders
class UserAPI {
private loader: DataLoader<string, User>;
constructor() {
this.loader = new DataLoader(async (ids: readonly string[]) => {
// Batch fetch users
const users = await db.user.findMany({
where: { id: { in: [...ids] } },
});
// Return in same order as input ids
const userMap = new Map(users.map(u => [u.id, u]));
return ids.map(id => userMap.get(id) ?? null);
});
}
async getUserById(id: string): Promise<User | null> {
return this.loader.load(id);
}
async getUsersByIds(ids: string[]): Promise<(User | null)[]> {
return this.loader.loadMany(ids);
}
}
// Usage in resolvers
const resolvers = {
Post: {
author: async (parent, _, { dataSources }) => {
// This will be batched with DataLoader
return dataSources.userAPI.getUserById(parent.authorId);
},
},
};GraphQL Codegen
GraphQL Codegen
yaml
undefinedyaml
undefinedcodegen.yml
codegen.yml
schema: './src/schema.graphql'
documents: './src/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
- typescript-operations
config:
useIndexSignature: true
contextType: '../context#Context'
mappers:
User: '../models#UserModel'
Post: '../models#PostModel'
```typescript
// Generated types usage
import { Resolvers } from './generated/graphql';
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
},
};schema: './src/schema.graphql'
documents: './src/**/*.graphql'
generates:
src/generated/graphql.ts:
plugins:
- typescript
- typescript-resolvers
- typescript-operations
config:
useIndexSignature: true
contextType: '../context#Context'
mappers:
User: '../models#UserModel'
Post: '../models#PostModel'
```typescript
// Generated types usage
import { Resolvers } from './generated/graphql';
const resolvers: Resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.userAPI.getUserById(id);
},
},
};Error Handling
错误处理
typescript
import { GraphQLError } from 'graphql';
class NotFoundError extends GraphQLError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, {
extensions: {
code: 'NOT_FOUND',
resource,
id,
},
});
}
}
class ValidationError extends GraphQLError {
constructor(message: string, field: string) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
field,
},
});
}
}
// Usage
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
const user = await dataSources.userAPI.getUserById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
},
},
};typescript
import { GraphQLError } from 'graphql';
class NotFoundError extends GraphQLError {
constructor(resource: string, id: string) {
super(`${resource} with id ${id} not found`, {
extensions: {
code: 'NOT_FOUND',
resource,
id,
},
});
}
}
class ValidationError extends GraphQLError {
constructor(message: string, field: string) {
super(message, {
extensions: {
code: 'BAD_USER_INPUT',
field,
},
});
}
}
// Usage
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
const user = await dataSources.userAPI.getUserById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
},
},
};Authentication & Authorization
认证与授权
typescript
// Context with user
interface Context {
user: User | null;
dataSources: DataSources;
}
// Auth directive
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';
function authDirective(directiveName: string) {
return {
authDirectiveTypeDefs: `directive @${directiveName}(requires: Role = USER) on OBJECT | FIELD_DEFINITION`,
authDirectiveTransformer: (schema: GraphQLSchema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
if (directive) {
const { resolve = defaultFieldResolver } = fieldConfig;
const { requires } = directive;
fieldConfig.resolve = async (source, args, context, info) => {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (requires && !context.user.roles.includes(requires)) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
},
}),
};
}
// Schema with directive
const typeDefs = `#graphql
enum Role {
USER
ADMIN
}
type Query {
user(id: ID!): User @auth
adminData: AdminData @auth(requires: ADMIN)
}
`;typescript
// Context with user
interface Context {
user: User | null;
dataSources: DataSources;
}
// Auth directive
import { getDirective, MapperKind, mapSchema } from '@graphql-tools/utils';
import { defaultFieldResolver, GraphQLSchema } from 'graphql';
function authDirective(directiveName: string) {
return {
authDirectiveTypeDefs: `directive @${directiveName}(requires: Role = USER) on OBJECT | FIELD_DEFINITION`,
authDirectiveTransformer: (schema: GraphQLSchema) =>
mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const directive = getDirective(schema, fieldConfig, directiveName)?.[0];
if (directive) {
const { resolve = defaultFieldResolver } = fieldConfig;
const { requires } = directive;
fieldConfig.resolve = async (source, args, context, info) => {
if (!context.user) {
throw new GraphQLError('Not authenticated', {
extensions: { code: 'UNAUTHENTICATED' },
});
}
if (requires && !context.user.roles.includes(requires)) {
throw new GraphQLError('Not authorized', {
extensions: { code: 'FORBIDDEN' },
});
}
return resolve(source, args, context, info);
};
}
return fieldConfig;
},
}),
};
}
// Schema with directive
const typeDefs = `#graphql
enum Role {
USER
ADMIN
}
type Query {
user(id: ID!): User @auth
adminData: AdminData @auth(requires: ADMIN)
}
`;Subscriptions with WebSocket
基于WebSocket的订阅
typescript
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// Create WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
// Setup subscription server
useServer(
{
schema,
context: async (ctx) => {
const token = ctx.connectionParams?.authentication;
const user = await getUserFromToken(token);
return { user, pubsub };
},
},
wsServer
);
// Subscription resolvers
const resolvers = {
Subscription: {
postPublished: {
subscribe: (_, __, { pubsub }) =>
pubsub.asyncIterator(['POST_PUBLISHED']),
},
messageAdded: {
subscribe: withFilter(
(_, __, { pubsub }) => pubsub.asyncIterator(['MESSAGE_ADDED']),
(payload, variables) => {
// Filter by channel
return payload.messageAdded.channelId === variables.channelId;
}
),
},
},
};typescript
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// Create WebSocket server
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
// Setup subscription server
useServer(
{
schema,
context: async (ctx) => {
const token = ctx.connectionParams?.authentication;
const user = await getUserFromToken(token);
return { user, pubsub };
},
},
wsServer
);
// Subscription resolvers
const resolvers = {
Subscription: {
postPublished: {
subscribe: (_, __, { pubsub }) =>
pubsub.asyncIterator(['POST_PUBLISHED']),
},
messageAdded: {
subscribe: withFilter(
(_, __, { pubsub }) => pubsub.asyncIterator(['MESSAGE_ADDED']),
(payload, variables) => {
// Filter by channel
return payload.messageAdded.channelId === variables.channelId;
}
),
},
},
};GraphQL Client (Apollo Client)
GraphQL客户端(Apollo Client)
typescript
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
});
// Query
const GET_USERS = gql`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
nodes {
id
name
email
}
totalCount
pageInfo {
hasNextPage
}
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS, {
variables: { limit: 10, offset: 0 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.nodes.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Mutation
const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
author {
name
}
}
}
`;
function CreatePostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
refetchQueries: ['GetPosts'],
});
const handleSubmit = async (e) => {
e.preventDefault();
await createPost({
variables: {
input: {
title: 'New Post',
content: 'Post content',
},
},
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
// Subscription
const POST_PUBLISHED = gql`
subscription OnPostPublished {
postPublished {
id
title
author {
name
}
}
}
`;
function PostFeed() {
const { data, loading } = useSubscription(POST_PUBLISHED);
if (loading) return <p>Waiting for posts...</p>;
return <div>New post: {data.postPublished.title}</div>;
}typescript
import { ApolloClient, InMemoryCache, gql, useQuery, useMutation } from '@apollo/client';
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
cache: new InMemoryCache(),
});
// Query
const GET_USERS = gql`
query GetUsers($limit: Int, $offset: Int) {
users(limit: $limit, offset: $offset) {
nodes {
id
name
email
}
totalCount
pageInfo {
hasNextPage
}
}
}
`;
function UserList() {
const { loading, error, data } = useQuery(GET_USERS, {
variables: { limit: 10, offset: 0 },
});
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{data.users.nodes.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
// Mutation
const CREATE_POST = gql`
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
author {
name
}
}
}
`;
function CreatePostForm() {
const [createPost, { loading, error }] = useMutation(CREATE_POST, {
refetchQueries: ['GetPosts'],
});
const handleSubmit = async (e) => {
e.preventDefault();
await createPost({
variables: {
input: {
title: 'New Post',
content: 'Post content',
},
},
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
// Subscription
const POST_PUBLISHED = gql`
subscription OnPostPublished {
postPublished {
id
title
author {
name
}
}
}
`;
function PostFeed() {
const { data, loading } = useSubscription(POST_PUBLISHED);
if (loading) return <p>Waiting for posts...</p>;
return <div>New post: {data.postPublished.title}</div>;
}Query Complexity & Depth Limiting
查询复杂度与深度限制
typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 10,
}),
],
plugins: [
{
async requestDidStart() {
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > 1000) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed: 1000`
);
}
},
};
},
},
],
});typescript
import { createComplexityLimitRule } from 'graphql-validation-complexity';
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
scalarCost: 1,
objectCost: 10,
listFactor: 10,
}),
],
plugins: [
{
async requestDidStart() {
return {
async didResolveOperation({ request, document }) {
const complexity = getComplexity({
schema,
query: document,
variables: request.variables,
estimators: [
fieldExtensionsEstimator(),
simpleEstimator({ defaultComplexity: 1 }),
],
});
if (complexity > 1000) {
throw new GraphQLError(
`Query is too complex: ${complexity}. Maximum allowed: 1000`
);
}
},
};
},
},
],
});GraphQL Federation
GraphQL联邦
Federated Schema
联邦Schema
typescript
// Users service
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
const resolvers = {
User: {
__resolveReference: async (reference, { dataSources }) => {
return dataSources.userAPI.getUserById(reference.id);
},
},
Query: {
user: (_, { id }, { dataSources }) => dataSources.userAPI.getUserById(id),
users: (_, __, { dataSources }) => dataSources.userAPI.getUsers(),
},
};
// Posts service
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
type Query {
post(id: ID!): Post
posts: [Post!]!
}
`;
const resolvers = {
Post: {
author: (post) => ({ __typename: 'User', id: post.authorId }),
},
User: {
posts: (user, _, { dataSources }) =>
dataSources.postAPI.getPostsByAuthorId(user.id),
},
};
// Gateway
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'posts', url: 'http://localhost:4002/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });typescript
// Users service
import { buildSubgraphSchema } from '@apollo/subgraph';
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type User @key(fields: "id") {
id: ID!
email: String!
name: String!
}
type Query {
user(id: ID!): User
users: [User!]!
}
`;
const resolvers = {
User: {
__resolveReference: async (reference, { dataSources }) => {
return dataSources.userAPI.getUserById(reference.id);
},
},
Query: {
user: (_, { id }, { dataSources }) => dataSources.userAPI.getUserById(id),
users: (_, __, { dataSources }) => dataSources.userAPI.getUsers(),
},
};
// Posts service
const typeDefs = gql`
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3")
type Post @key(fields: "id") {
id: ID!
title: String!
content: String!
author: User!
}
extend type User @key(fields: "id") {
id: ID! @external
posts: [Post!]!
}
type Query {
post(id: ID!): Post
posts: [Post!]!
}
`;
const resolvers = {
Post: {
author: (post) => ({ __typename: 'User', id: post.authorId }),
},
User: {
posts: (user, _, { dataSources }) =>
dataSources.postAPI.getPostsByAuthorId(user.id),
},
};
// Gateway
import { ApolloGateway, IntrospectAndCompose } from '@apollo/gateway';
const gateway = new ApolloGateway({
supergraphSdl: new IntrospectAndCompose({
subgraphs: [
{ name: 'users', url: 'http://localhost:4001/graphql' },
{ name: 'posts', url: 'http://localhost:4002/graphql' },
],
}),
});
const server = new ApolloServer({ gateway });Best Practices
最佳实践
Schema Design
Schema设计
graphql
undefinedgraphql
undefinedUse clear, consistent naming
Use clear, consistent naming
type User {
id: ID!
email: String!
createdAt: DateTime!
}
type User {
id: ID!
email: String!
createdAt: DateTime!
}
Prefer input types over many arguments
Prefer input types over many arguments
input CreateUserInput {
email: String!
name: String!
}
mutation {
createUser(input: CreateUserInput!): User!
}
input CreateUserInput {
email: String!
name: String!
}
mutation {
createUser(input: CreateUserInput!): User!
}
Use enums for fixed sets
Use enums for fixed sets
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
}
Design for pagination
Design for pagination
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
undefinedtype PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
undefinedPerformance Optimization
性能优化
- Use DataLoader for batching and caching
- Implement query complexity analysis
- Add depth limiting
- Use persisted queries for production
- Cache at multiple levels (CDN, field-level, full response)
- Monitor query performance and slow fields
- 使用DataLoader进行批处理与缓存
- 实现查询复杂度分析
- 添加深度限制
- 生产环境使用持久化查询
- 多级缓存(CDN、字段级、全响应)
- 监控查询性能与慢字段
Security
安全
- Validate and sanitize all inputs
- Implement rate limiting
- Use query depth and complexity limits
- Sanitize error messages in production
- Implement proper authentication and authorization
- Use HTTPS for all connections
- Validate file uploads (type, size)
- 验证并清理所有输入
- 实现速率限制
- 使用查询深度与复杂度限制
- 生产环境中清理错误信息
- 实现正确的认证与授权
- 所有连接使用HTTPS
- 验证文件上传(类型、大小)
Anti-Patterns to Avoid
需避免的反模式
❌ Exposing internal IDs: Use opaque IDs or UUIDs
❌ Overly nested queries: Limit query depth
❌ No pagination: Always paginate lists
❌ Resolving in mutations: Keep mutations focused
❌ Exposing database schema directly: Design API-first
❌ No DataLoader: Leads to N+1 queries
❌ Generic error messages: Provide actionable errors
❌ No versioning strategy: Plan for schema evolution
❌ 暴露内部ID:使用不透明ID或UUID
❌ 过度嵌套查询:限制查询深度
❌ 未实现分页:列表必须分页
❌ 变更中包含解析逻辑:保持变更功能聚焦
❌ 直接暴露数据库Schema:采用API优先设计
❌ 未使用DataLoader:导致N+1查询问题
❌ 通用错误信息:提供可操作的错误提示
❌ 无版本化策略:提前规划Schema演进
Testing
测试
typescript
import { ApolloServer } from '@apollo/server';
import { describe, it, expect } from 'vitest';
describe('GraphQL Server', () => {
it('should fetch user by id', async () => {
const server = new ApolloServer({ typeDefs, resolvers });
const response = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '1' },
});
expect(response.body.kind).toBe('single');
expect(response.body.singleResult.data?.user).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com',
});
});
it('should create post', async () => {
const response = await server.executeOperation({
query: `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
`,
variables: {
input: {
title: 'Test Post',
content: 'Content',
},
},
});
expect(response.body.singleResult.data?.createPost).toHaveProperty('id');
});
});typescript
import { ApolloServer } from '@apollo/server';
import { describe, it, expect } from 'vitest';
describe('GraphQL Server', () => {
it('should fetch user by id', async () => {
const server = new ApolloServer({ typeDefs, resolvers });
const response = await server.executeOperation({
query: `
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
}
}
`,
variables: { id: '1' },
});
expect(response.body.kind).toBe('single');
expect(response.body.singleResult.data?.user).toEqual({
id: '1',
name: 'Alice',
email: 'alice@example.com',
});
});
it('should create post', async () => {
const response = await server.executeOperation({
query: `
mutation CreatePost($input: CreatePostInput!) {
createPost(input: $input) {
id
title
}
}
`,
variables: {
input: {
title: 'Test Post',
content: 'Content',
},
},
});
expect(response.body.singleResult.data?.createPost).toHaveProperty('id');
});
});Common Patterns
常见模式
Relay Cursor Pagination
Relay游标分页
graphql
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}graphql
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
}
type PostEdge {
cursor: String!
node: Post!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}File Upload
文件上传
typescript
import { GraphQLUpload } from 'graphql-upload-ts';
const typeDefs = gql`
scalar Upload
type Mutation {
uploadFile(file: Upload!): File!
}
`;
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadFile: async (_, { file }) => {
const { createReadStream, filename, mimetype } = await file;
const stream = createReadStream();
// Process upload
await saveFile(stream, filename);
return { id: '1', filename, mimetype };
},
},
};typescript
import { GraphQLUpload } from 'graphql-upload-ts';
const typeDefs = gql`
scalar Upload
type Mutation {
uploadFile(file: Upload!): File!
}
`;
const resolvers = {
Upload: GraphQLUpload,
Mutation: {
uploadFile: async (_, { file }) => {
const { createReadStream, filename, mimetype } = await file;
const stream = createReadStream();
// Process upload
await saveFile(stream, filename);
return { id: '1', filename, mimetype };
},
},
};Resources
资源
- Apollo Server: https://www.apollographql.com/docs/apollo-server/
- GraphQL Spec: https://spec.graphql.org/
- DataLoader: https://github.com/graphql/dataloader
- GraphQL Code Generator: https://the-guild.dev/graphql/codegen
- GraphQL Tools: https://the-guild.dev/graphql/tools
- Apollo Server: https://www.apollographql.com/docs/apollo-server/
- GraphQL Spec: https://spec.graphql.org/
- DataLoader: https://github.com/graphql/dataloader
- GraphQL Code Generator: https://the-guild.dev/graphql/codegen
- GraphQL Tools: https://the-guild.dev/graphql/tools