graphql-expert

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

GraphQL Expert

GraphQL 专家指南

Comprehensive guide for designing and implementing GraphQL APIs.
GraphQL API设计与实现的综合指南。

GraphQL Fundamentals

GraphQL 基础概念

What is GraphQL?

什么是GraphQL?

GraphQL is a query language for APIs that:
✓ Lets clients request exactly what they need
✓ Gets multiple resources in one request
✓ Uses a type system to describe data
✓ Provides introspection (self-documenting)

GraphQL vs REST:
┌─────────────────────────────────────────┐
│ REST: Multiple endpoints, fixed shapes  │
│ GET /users/1                            │
│ GET /users/1/posts                      │
│ GET /users/1/followers                  │
├─────────────────────────────────────────┤
│ GraphQL: Single endpoint, flexible      │
│ POST /graphql                           │
│ query { user(id: 1) {                   │
│   name                                  │
│   posts { title }                       │
│   followers { name }                    │
│ }}                                      │
└─────────────────────────────────────────┘

GraphQL is a query language for APIs that:
✓ Lets clients request exactly what they need
✓ Gets multiple resources in one request
✓ Uses a type system to describe data
✓ Provides introspection (self-documenting)

GraphQL vs REST:
┌─────────────────────────────────────────┐
│ REST: Multiple endpoints, fixed shapes  │
│ GET /users/1                            │
│ GET /users/1/posts                      │
│ GET /users/1/followers                  │
├─────────────────────────────────────────┤
│ GraphQL: Single endpoint, flexible      │
│ POST /graphql                           │
│ query { user(id: 1) {                   │
│   name                                  │
│   posts { title }                       │
│   followers { name }                    │
│ }}                                      │
└─────────────────────────────────────────┘

Schema Design

Schema 设计

Type System

类型系统

graphql
undefined
graphql
undefined

Scalar Types (built-in)

Scalar Types (built-in)

String, Int, Float, Boolean, ID
String, Int, Float, Boolean, ID

Custom Scalar

Custom Scalar

scalar DateTime scalar JSON
scalar DateTime scalar JSON

Object Type

Object Type

type User { id: ID! email: String! name: String createdAt: DateTime! posts: [Post!]! }
type User { id: ID! email: String! name: String createdAt: DateTime! posts: [Post!]! }

Enum

Enum

enum Role { ADMIN USER GUEST }
enum Role { ADMIN USER GUEST }

Interface

Interface

interface Node { id: ID! }
type User implements Node { id: ID!

... other fields

}
interface Node { id: ID! }
type User implements Node { id: ID!

... other fields

}

Union

Union

union SearchResult = User | Post | Comment
union SearchResult = User | Post | Comment

Input Type (for mutations)

Input Type (for mutations)

input CreateUserInput { email: String! name: String! role: Role = USER }
undefined
input CreateUserInput { email: String! name: String! role: Role = USER }
undefined

Nullability

可空性

graphql
undefined
graphql
undefined

Field modifiers:

Field modifiers:

String # Nullable string String! # Non-null string [String] # Nullable list of nullable strings [String!] # Nullable list of non-null strings [String]! # Non-null list of nullable strings [String!]! # Non-null list of non-null strings
String # 可空字符串 String! # 非空字符串 [String] # 可空列表,元素为可空字符串 [String!] # 可空列表,元素为非空字符串 [String]! # 非空列表,元素为可空字符串 [String!]! # 非空列表,元素为非空字符串

Best practice:

最佳实践:

- Make fields nullable by default

- 默认将字段设为可空

- Use ! only when guaranteed non-null

- 仅在确保非空时使用 !

- Lists should usually be non-null: [Item!]!

- 列表通常应设为非空: [Item!]!

undefined
undefined

Schema Structure

Schema 结构

graphql
undefined
graphql
undefined

Root Types

Root Types

type Query {

Read operations

user(id: ID!): User users(limit: Int, offset: Int): [User!]! me: User }
type Mutation {

Write operations

createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean! }
type Subscription {

Real-time updates

userCreated: User! messageReceived(roomId: ID!): Message! }

---
type Query {

读取操作

user(id: ID!): User users(limit: Int, offset: Int): [User!]! me: User }
type Mutation {

写入操作

createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean! }
type Subscription {

实时更新

userCreated: User! messageReceived(roomId: ID!): Message! }

---

Query Design

查询设计

Basic Queries

基础查询

graphql
undefined
graphql
undefined

Simple query

Simple query

query GetUser { user(id: "1") { name email } }
query GetUser { user(id: "1") { name email } }

With variables

With variables

query GetUser($id: ID!) { user(id: $id) { name email } }
query GetUser($id: ID!) { user(id: $id) { name email } }

Multiple queries

Multiple queries

query Dashboard { me { name notifications { count } } recentPosts(limit: 5) { title createdAt } }
undefined
query Dashboard { me { name notifications { count } } recentPosts(limit: 5) { title createdAt } }
undefined

Pagination

分页

graphql
undefined
graphql
undefined

Offset-based (simple, but has issues)

Offset-based (simple, but has issues)

type Query { users(limit: Int!, offset: Int!): [User!]! }
type Query { users(limit: Int!, offset: Int!): [User!]! }

Cursor-based (recommended)

Cursor-based (recommended)

type Query { users(first: Int, after: String): UserConnection! }
type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! }
type UserEdge { cursor: String! node: User! }
type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }
type Query { users(first: Int, after: String): UserConnection! }
type UserConnection { edges: [UserEdge!]! pageInfo: PageInfo! totalCount: Int! }
type UserEdge { cursor: String! node: User! }
type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String }

Usage

Usage

query { users(first: 10, after: "cursor123") { edges { cursor node { name email } } pageInfo { hasNextPage endCursor } } }
undefined
query { users(first: 10, after: "cursor123") { edges { cursor node { name email } } pageInfo { hasNextPage endCursor } } }
undefined

Filtering & Sorting

过滤与排序

graphql
input UserFilter {
  name: StringFilter
  email: StringFilter
  role: Role
  createdAt: DateFilter
}

input StringFilter {
  equals: String
  contains: String
  startsWith: String
}

input DateFilter {
  before: DateTime
  after: DateTime
}

enum UserSortField {
  NAME
  EMAIL
  CREATED_AT
}

input UserSort {
  field: UserSortField!
  direction: SortDirection!
}

enum SortDirection {
  ASC
  DESC
}

type Query {
  users(
    filter: UserFilter
    sort: UserSort
    first: Int
    after: String
  ): UserConnection!
}

graphql
input UserFilter {
  name: StringFilter
  email: StringFilter
  role: Role
  createdAt: DateFilter
}

input StringFilter {
  equals: String
  contains: String
  startsWith: String
}

input DateFilter {
  before: DateTime
  after: DateTime
}

enum UserSortField {
  NAME
  EMAIL
  CREATED_AT
}

input UserSort {
  field: UserSortField!
  direction: SortDirection!
}

enum SortDirection {
  ASC
  DESC
}

type Query {
  users(
    filter: UserFilter
    sort: UserSort
    first: Int
    after: String
  ): UserConnection!
}

Mutations

变更操作(Mutation)

Mutation Design

变更设计

graphql
undefined
graphql
undefined

Input types for mutations

Input types for mutations

input CreatePostInput { title: String! content: String! published: Boolean = false categoryIds: [ID!] }
input UpdatePostInput { title: String content: String published: Boolean }
input CreatePostInput { title: String! content: String! published: Boolean = false categoryIds: [ID!] }
input UpdatePostInput { title: String content: String published: Boolean }

Mutation payloads (recommended)

Mutation payloads (recommended)

type CreatePostPayload { post: Post errors: [Error!]! }
type Error { field: String message: String! }
type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! updatePost(id: ID!, input: UpdatePostInput!): UpdatePostPayload! deletePost(id: ID!): DeletePostPayload! }
type CreatePostPayload { post: Post errors: [Error!]! }
type Error { field: String message: String! }
type Mutation { createPost(input: CreatePostInput!): CreatePostPayload! updatePost(id: ID!, input: UpdatePostInput!): CreatePostPayload! deletePost(id: ID!): DeletePostPayload! }

Usage

Usage

mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { post { id title } errors { field message } } }
undefined
mutation CreatePost($input: CreatePostInput!) { createPost(input: $input) { post { id title } errors { field message } } }
undefined

Batch Mutations

批量变更

graphql
undefined
graphql
undefined

For multiple operations

For multiple operations

type Mutation { bulkDeletePosts(ids: [ID!]!): BulkDeletePayload! bulkUpdatePosts(updates: [PostUpdate!]!): BulkUpdatePayload! }
input PostUpdate { id: ID! input: UpdatePostInput! }

---
type Mutation { bulkDeletePosts(ids: [ID!]!): BulkDeletePayload! bulkUpdatePosts(updates: [PostUpdate!]!): BulkUpdatePayload! }
input PostUpdate { id: ID! input: UpdatePostInput! }

---

Resolvers

Resolver 实现

Basic Resolvers

基础Resolver

typescript
// TypeScript resolver implementation
const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.dataSources.users.findById(id);
    },

    users: async (_, { filter, sort, first, after }, context) => {
      return context.dataSources.users.findMany({
        filter,
        sort,
        first,
        after,
      });
    },
  },

  Mutation: {
    createUser: async (_, { input }, context) => {
      // Validate
      const errors = validateCreateUser(input);
      if (errors.length) {
        return { user: null, errors };
      }

      // Create
      const user = await context.dataSources.users.create(input);
      return { user, errors: [] };
    },
  },

  // Field resolvers
  User: {
    posts: async (user, _, context) => {
      return context.dataSources.posts.findByUserId(user.id);
    },

    fullName: (user) => {
      return `${user.firstName} ${user.lastName}`;
    },
  },
};
typescript
// TypeScript resolver implementation
const resolvers = {
  Query: {
    user: async (_, { id }, context) => {
      return context.dataSources.users.findById(id);
    },

    users: async (_, { filter, sort, first, after }, context) => {
      return context.dataSources.users.findMany({
        filter,
        sort,
        first,
        after,
      });
    },
  },

  Mutation: {
    createUser: async (_, { input }, context) => {
      // Validate
      const errors = validateCreateUser(input);
      if (errors.length) {
        return { user: null, errors };
      }

      // Create
      const user = await context.dataSources.users.create(input);
      return { user, errors: [] };
    },
  },

  // Field resolvers
  User: {
    posts: async (user, _, context) => {
      return context.dataSources.posts.findByUserId(user.id);
    },

    fullName: (user) => {
      return `${user.firstName} ${user.lastName}`;
    },
  },
};

Resolver Context

Resolver 上下文

typescript
// Context setup
interface Context {
  user: User | null;
  dataSources: {
    users: UserDataSource;
    posts: PostDataSource;
  };
  loaders: {
    userLoader: DataLoader<string, User>;
    postLoader: DataLoader<string, Post>;
  };
}

// In server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => ({
    user: await getUserFromToken(req.headers.authorization),
    dataSources: {
      users: new UserDataSource(db),
      posts: new PostDataSource(db),
    },
    loaders: createLoaders(),
  }),
});

typescript
// Context setup
interface Context {
  user: User | null;
  dataSources: {
    users: UserDataSource;
    posts: PostDataSource;
  };
  loaders: {
    userLoader: DataLoader<string, User>;
    postLoader: DataLoader<string, Post>;
  };
}

// In server setup
const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => ({
    user: await getUserFromToken(req.headers.authorization),
    dataSources: {
      users: new UserDataSource(db),
      posts: new PostDataSource(db),
    },
    loaders: createLoaders(),
  }),
});

Performance Optimization

性能优化

DataLoader (N+1 Solution)

DataLoader(解决N+1问题)

typescript
import DataLoader from "dataloader";

// Create loader
const userLoader = new DataLoader<string, User>(async (ids) => {
  const users = await db.users.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);
});

// Use in resolver
const resolvers = {
  Post: {
    author: (post, _, context) => {
      return context.loaders.userLoader.load(post.authorId);
    },
  },
};
typescript
import DataLoader from "dataloader";

// Create loader
const userLoader = new DataLoader<string, User>(async (ids) => {
  const users = await db.users.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);
});

// Use in resolver
const resolvers = {
  Post: {
    author: (post, _, context) => {
      return context.loaders.userLoader.load(post.authorId);
    },
  },
};

Query Complexity

查询复杂度控制

typescript
import { createComplexityLimitRule } from "graphql-validation-complexity";

// Limit query complexity
const complexityLimitRule = createComplexityLimitRule(1000, {
  scalarCost: 1,
  objectCost: 10,
  listFactor: 10,
});

// Or field-level costs
const typeDefs = gql`
  type Query {
    users: [User!]! @complexity(value: 10, multipliers: ["first"])
    user(id: ID!): User @complexity(value: 1)
  }
`;
typescript
import { createComplexityLimitRule } from "graphql-validation-complexity";

// Limit query complexity
const complexityLimitRule = createComplexityLimitRule(1000, {
  scalarCost: 1,
  objectCost: 10,
  listFactor: 10,
});

// Or field-level costs
const typeDefs = gql`
  type Query {
    users: [User!]! @complexity(value: 10, multipliers: ["first"])
    user(id: ID!): User @complexity(value: 1)
  }
`;

Query Depth Limiting

查询深度限制

typescript
import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)],
});
typescript
import depthLimit from "graphql-depth-limit";

const server = new ApolloServer({
  typeDefs,
  resolvers,
  validationRules: [depthLimit(10)],
});

Persisted Queries

持久化查询

typescript
// Client sends hash instead of full query
// Reduces bandwidth, enables whitelisting

// Apollo Client setup
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";

const link = createPersistedQueryLink({ sha256 }).concat(httpLink);

// Server validates against known queries
const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new RedisCache(),
  },
});

typescript
// Client sends hash instead of full query
// Reduces bandwidth, enables whitelisting

// Apollo Client setup
import { createPersistedQueryLink } from "@apollo/client/link/persisted-queries";

const link = createPersistedQueryLink({ sha256 }).concat(httpLink);

// Server validates against known queries
const server = new ApolloServer({
  typeDefs,
  resolvers,
  persistedQueries: {
    cache: new RedisCache(),
  },
});

Authentication & Authorization

认证与授权

Context-Based Auth

基于上下文的认证

typescript
// Add user to context
const context = async ({ req }) => {
  const token = req.headers.authorization?.replace("Bearer ", "");
  const user = token ? await verifyToken(token) : null;
  return { user };
};

// Check in resolvers
const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new AuthenticationError("Not authenticated");
      }
      return context.user;
    },
  },
};
typescript
// Add user to context
const context = async ({ req }) => {
  const token = req.headers.authorization?.replace("Bearer ", "");
  const user = token ? await verifyToken(token) : null;
  return { user };
};

// Check in resolvers
const resolvers = {
  Query: {
    me: (_, __, context) => {
      if (!context.user) {
        throw new AuthenticationError("未认证");
      }
      return context.user;
    },
  },
};

Directive-Based Auth

基于指令的授权

graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION

type Query {
  publicPosts: [Post!]!
  me: User @auth
  adminDashboard: Dashboard @auth(requires: ADMIN)
}
typescript
// Directive implementation
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const requiredRole = this.args.requires;

    field.resolve = async function (...args) {
      const context = args[2];

      if (!context.user) {
        throw new AuthenticationError("Not authenticated");
      }

      if (requiredRole && context.user.role !== requiredRole) {
        throw new ForbiddenError("Not authorized");
      }

      return resolve.apply(this, args);
    };
  }
}

graphql
directive @auth(requires: Role = USER) on FIELD_DEFINITION

type Query {
  publicPosts: [Post!]!
  me: User @auth
  adminDashboard: Dashboard @auth(requires: ADMIN)
}
typescript
// Directive implementation
class AuthDirective extends SchemaDirectiveVisitor {
  visitFieldDefinition(field) {
    const { resolve = defaultFieldResolver } = field;
    const requiredRole = this.args.requires;

    field.resolve = async function (...args) {
      const context = args[2];

      if (!context.user) {
        throw new AuthenticationError("未认证");
      }

      if (requiredRole && context.user.role !== requiredRole) {
        throw new ForbiddenError("无权限访问");
      }

      return resolve.apply(this, args);
    };
  }
}

Error Handling

错误处理

Error Types

错误类型

typescript
import {
  ApolloError,
  AuthenticationError,
  ForbiddenError,
  UserInputError,
} from "apollo-server";

// Validation errors
throw new UserInputError("Invalid email format", {
  field: "email",
});

// Auth errors
throw new AuthenticationError("Must be logged in");
throw new ForbiddenError("Not authorized to view this resource");

// Custom errors
class NotFoundError extends ApolloError {
  constructor(resource: string) {
    super(`${resource} not found`, "NOT_FOUND");
  }
}
typescript
import {
  ApolloError,
  AuthenticationError,
  ForbiddenError,
  UserInputError,
} from "apollo-server";

// Validation errors
throw new UserInputError("邮箱格式无效", {
  field: "email",
});

// Auth errors
throw new AuthenticationError("请先登录");
throw new ForbiddenError("无权限访问该资源");

// Custom errors
class NotFoundError extends ApolloError {
  constructor(resource: string) {
    super(`${resource} 不存在`, "NOT_FOUND");
  }
}

Error Formatting

错误格式化

typescript
const server = new ApolloServer({
  formatError: (error) => {
    // Log internal errors
    if (error.extensions?.code === "INTERNAL_SERVER_ERROR") {
      console.error(error);
      return new Error("Internal server error");
    }

    // Mask sensitive info
    if (error.message.includes("password")) {
      return new Error("An error occurred");
    }

    return error;
  },
});

typescript
const server = new ApolloServer({
  formatError: (error) => {
    // Log internal errors
    if (error.extensions?.code === "INTERNAL_SERVER_ERROR") {
      console.error(error);
      return new Error("内部服务器错误");
    }

    // Mask sensitive info
    if (error.message.includes("password")) {
      return new Error("发生错误");
    }

    return error;
  },
});

Subscriptions

订阅功能

Setup

配置

graphql
type Subscription {
  messageCreated(roomId: ID!): Message!
  userStatusChanged(userId: ID!): UserStatus!
}
typescript
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();

const resolvers = {
  Mutation: {
    sendMessage: async (_, { input }, context) => {
      const message = await createMessage(input);

      pubsub.publish(`MESSAGE_CREATED_${input.roomId}`, {
        messageCreated: message,
      });

      return message;
    },
  },

  Subscription: {
    messageCreated: {
      subscribe: (_, { roomId }) => {
        return pubsub.asyncIterator(`MESSAGE_CREATED_${roomId}`);
      },
    },
  },
};
graphql
type Subscription {
  messageCreated(roomId: ID!): Message!
  userStatusChanged(userId: ID!): UserStatus!
}
typescript
import { PubSub } from "graphql-subscriptions";

const pubsub = new PubSub();

const resolvers = {
  Mutation: {
    sendMessage: async (_, { input }, context) => {
      const message = await createMessage(input);

      pubsub.publish(`MESSAGE_CREATED_${input.roomId}`, {
        messageCreated: message,
      });

      return message;
    },
  },

  Subscription: {
    messageCreated: {
      subscribe: (_, { roomId }) => {
        return pubsub.asyncIterator(`MESSAGE_CREATED_${roomId}`);
      },
    },
  },
};

With Filtering

带过滤的订阅

typescript
import { withFilter } from "graphql-subscriptions";

const resolvers = {
  Subscription: {
    messageCreated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator("MESSAGE_CREATED"),
        (payload, variables, context) => {
          // Only send to users in the room
          return (
            payload.messageCreated.roomId === variables.roomId &&
            context.user.rooms.includes(variables.roomId)
          );
        },
      ),
    },
  },
};

typescript
import { withFilter } from "graphql-subscriptions";

const resolvers = {
  Subscription: {
    messageCreated: {
      subscribe: withFilter(
        () => pubsub.asyncIterator("MESSAGE_CREATED"),
        (payload, variables, context) => {
          // 仅向房间内用户发送消息
          return (
            payload.messageCreated.roomId === variables.roomId &&
            context.user.rooms.includes(variables.roomId)
          );
        },
      ),
    },
  },
};

Client Integration

客户端集成

Apollo Client Setup

Apollo Client 配置

typescript
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

const httpLink = createHttpLink({
  uri: "/graphql",
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("token");
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
});
typescript
import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client";
import { setContext } from "@apollo/client/link/context";

const httpLink = createHttpLink({
  uri: "/graphql",
});

const authLink = setContext((_, { headers }) => {
  const token = localStorage.getItem("token");
  return {
    headers: {
      ...headers,
      authorization: token ? `Bearer ${token}` : "",
    },
  };
});

const client = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache(),
});

React Hooks

React Hooks

typescript
import { useQuery, useMutation, gql } from '@apollo/client';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <div>{data.user.name}</div>;
}

// Mutation
const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      post { id title }
      errors { field message }
    }
  }
`;

function CreatePostForm() {
  const [createPost, { loading }] = useMutation(CREATE_POST, {
    update(cache, { data }) {
      // Update cache after mutation
    },
  });

  const handleSubmit = async (input) => {
    const { data } = await createPost({ variables: { input } });
    if (data.createPost.errors.length) {
      // Handle errors
    }
  };
}

typescript
import { useQuery, useMutation, gql } from '@apollo/client';

const GET_USER = gql`
  query GetUser($id: ID!) {
    user(id: $id) {
      id
      name
      email
    }
  }
`;

function UserProfile({ userId }) {
  const { loading, error, data } = useQuery(GET_USER, {
    variables: { id: userId },
  });

  if (loading) return <Spinner />;
  if (error) return <Error message={error.message} />;

  return <div>{data.user.name}</div>;
}

// Mutation
const CREATE_POST = gql`
  mutation CreatePost($input: CreatePostInput!) {
    createPost(input: $input) {
      post { id title }
      errors { field message }
    }
  }
`;

function CreatePostForm() {
  const [createPost, { loading }] = useMutation(CREATE_POST, {
    update(cache, { data }) {
      // 变更后更新缓存
    },
  });

  const handleSubmit = async (input) => {
    const { data } = await createPost({ variables: { input } });
    if (data.createPost.errors.length) {
      // 处理错误
    }
  };
}

Best Practices

最佳实践

DO:

建议:

  • Design schema from client perspective
  • Use input types for mutations
  • Return payloads with errors from mutations
  • Implement DataLoader for N+1 prevention
  • Use cursor-based pagination
  • Add query complexity limits
  • Version schemas carefully
  • 从客户端视角设计Schema
  • 变更操作使用输入类型
  • 变更操作返回包含错误信息的Payload
  • 实现DataLoader解决N+1问题
  • 使用基于游标的分页
  • 添加查询复杂度限制
  • 谨慎进行Schema版本管理

DON'T:

不建议:

  • Expose database schema directly
  • Create deeply nested types unnecessarily
  • Forget about authorization
  • Allow unbounded queries
  • Skip error handling
  • Ignore caching strategies
  • Mutate data in queries

  • 直接暴露数据库Schema
  • 不必要地创建深度嵌套类型
  • 忽略授权机制
  • 允许无限制的查询
  • 跳过错误处理
  • 忽略缓存策略
  • 在查询中修改数据

Schema Checklist

Schema 检查清单

  • Types named descriptively (singular, PascalCase)
  • Consistent nullability patterns
  • Input types for all mutations
  • Payload types with error handling
  • Pagination for all lists
  • Authentication/authorization considered
  • Performance optimizations in place
  • 类型命名具有描述性(单数、大驼峰)
  • 可空性模式保持一致
  • 所有变更操作使用输入类型
  • Payload类型包含错误处理
  • 所有列表支持分页
  • 考虑了认证/授权机制
  • 已实施性能优化措施