GraphQL Schema 设计最佳实践
GraphQL Schema 是 API 的核心,良好的 Schema 设计能够提高开发效率、减少维护成本,并提供更好的开发者体验。
1. Schema 基础结构
类型定义
graphqltype User { id: ID! name: String! email: String! age: Int posts: [Post!]! createdAt: DateTime! } type Post { id: ID! title: String! content: String! author: User! comments: [Comment!]! createdAt: DateTime! } type Comment { id: ID! text: String! author: User! post: Post! createdAt: DateTime! }
输入类型
graphqlinput CreateUserInput { name: String! email: String! age: Int } input CreatePostInput { title: String! content: String! authorId: ID! }
枚举类型
graphqlenum PostStatus { DRAFT PUBLISHED ARCHIVED }
联合类型
graphqlunion SearchResult = User | Post | Comment
接口类型
graphqlinterface Node { id: ID! } type User implements Node { id: ID! name: String! email: String! } type Post implements Node { id: ID! title: String! content: String! }
2. 命名规范
类型命名
- 使用 PascalCase(首字母大写)
- 使用描述性名称
- 避免缩写
- 使用单数形式
示例:
graphql# 好的命名 type UserProfile { } type Article { } type ShoppingCart { } # 不好的命名 type user { } type Art { } type ShopCart { }
字段命名
- 使用 camelCase(首字母小写)
- 使用动词或名词
- 避免使用保留字
示例:
graphql# 好的命名 type User { firstName: String! lastName: String! fullName: String! isActive: Boolean! } # 不好的命名 type User { first_name: String! LastName: String! Full_Name: String! active: Boolean! }
参数命名
- 使用 camelCase
- 描述性名称
- 包含单位或类型信息
示例:
graphql# 好的命名 query GetUsers($limit: Int, $offset: Int) { } query GetPosts($after: String, $first: Int) { } # 不好的命名 query GetUsers($l: Int, $o: Int) { } query GetPosts($a: String, $f: Int) { }
3. 类型设计原则
避免过度嵌套
graphql# 不好的设计 - 过度嵌套 type User { posts { comments { author { posts { comments { # 无限嵌套 } } } } } } # 好的设计 - 合理嵌套 type User { posts(limit: Int): [Post!]! } type Post { comments(limit: Int): [Comment!]! }
使用输入类型封装参数
graphql# 不好的设计 - 参数过多 mutation CreateUser($name: String!, $email: String!, $age: Int, $address: String, $phone: String) { } # 好的设计 - 使用输入类型 input CreateUserInput { name: String! email: String! age: Int address: String phone: String } mutation CreateUser($input: CreateUserInput!) { }
实现分页
graphql# 基于游标的分页(推荐) type PageInfo { hasNextPage: Boolean! hasPreviousPage: Boolean! startCursor: String endCursor: String } type PostConnection { edges: [PostEdge!]! pageInfo: PageInfo! totalCount: Int! } type PostEdge { node: Post! cursor: String! } type Query { posts(after: String, first: Int): PostConnection! } # 基于偏移的分页 type PostResult { posts: [Post!]! total: Int! page: Int! pageSize: Int! } type Query { posts(offset: Int, limit: Int): PostResult! }
4. 错误处理
自定义错误类型
graphqltype Error { code: String! message: String! field: String } type UserResult { user: User errors: [Error!]! } type Mutation { createUser(input: CreateUserInput!): UserResult! }
使用可空字段
graphqltype Mutation { # 返回可空类型表示可能失败 createUser(input: CreateUserInput!): User }
5. 版本控制与演进
废弃字段
graphqltype User { id: ID! name: String! # 废弃字段,提供替代方案 fullName: String @deprecated(reason: "Use 'name' instead") email: String! }
添加新字段
graphqltype User { id: ID! name: String! email: String! # 新增字段,不影响现有客户端 phoneNumber: String }
6. 性能优化
使用 DataLoader
javascriptconst userLoader = new DataLoader(async (userIds) => { const users = await User.findAll({ where: { id: userIds } }); return userIds.map(id => users.find(user => user.id === id)); });
字段级权限控制
graphqltype User { id: ID! name: String! # 敏感字段需要权限 email: String! @auth(requires: ADMIN) salary: Float @auth(requires: ADMIN) }
7. 文档与描述
添加描述
graphql""" 用户类型,包含用户的基本信息 """ type User { """ 用户唯一标识符 """ id: ID! """ 用户姓名,最大长度 100 字符 """ name: String! """ 用户邮箱地址,必须是有效的邮箱格式 """ email: String! }
8. 最佳实践总结
- 保持简洁: 避免过度设计,保持 Schema 简洁明了
- 一致性: 在整个项目中保持命名和结构的一致性
- 可扩展性: 设计时考虑未来的扩展需求
- 向后兼容: 添加新功能时保持向后兼容
- 文档化: 为所有类型、字段和参数添加描述
- 性能考虑: 避免过度嵌套,合理使用分页
- 安全性: 实现适当的权限控制和验证
- 测试: 为 Resolver 编写单元测试和集成测试