乐闻世界logo
搜索文章和话题

GraphQL 项目开发有哪些最佳实践

2月21日 17:00

GraphQL 项目开发最佳实践

在实际项目中使用 GraphQL 时,遵循最佳实践可以帮助团队构建可维护、可扩展和高质量的 GraphQL API。以下是项目开发中的关键最佳实践。

1. 项目结构组织

推荐的项目结构

shell
graphql-project/ ├── src/ │ ├── graphql/ │ │ ├── schema/ │ │ │ ├── index.graphql │ │ │ ├── types/ │ │ │ │ ├── user.graphql │ │ │ │ ├── post.graphql │ │ │ │ └── comment.graphql │ │ │ ├── queries/ │ │ │ │ └── index.graphql │ │ │ ├── mutations/ │ │ │ │ └── index.graphql │ │ │ └── subscriptions/ │ │ │ └── index.graphql │ │ ├── resolvers/ │ │ │ ├── index.ts │ │ │ ├── user.resolver.ts │ │ │ ├── post.resolver.ts │ │ │ └── comment.resolver.ts │ │ ├── directives/ │ │ │ ├── auth.directive.ts │ │ │ ├── cache.directive.ts │ │ │ └── index.ts │ │ ├── loaders/ │ │ │ ├── user.loader.ts │ │ │ ├── post.loader.ts │ │ │ └── index.ts │ │ ├── utils/ │ │ │ ├── schema-merger.ts │ │ │ ├── validator.ts │ │ │ └── error-handler.ts │ │ └── context.ts │ ├── models/ │ │ ├── User.ts │ │ ├── Post.ts │ │ └── Comment.ts │ ├── services/ │ │ ├── userService.ts │ │ ├── postService.ts │ │ └── commentService.ts │ └── index.ts ├── tests/ │ ├── unit/ │ │ └── resolvers/ │ └── integration/ │ └── api/ ├── package.json └── tsconfig.json

模块化 Schema

graphql
# types/user.graphql type User { id: ID! name: String! email: String! createdAt: DateTime! updatedAt: DateTime! } input CreateUserInput { name: String! email: String! } input UpdateUserInput { name: String email: String }
graphql
# queries/index.graphql extend type Query { user(id: ID!): User users(limit: Int, offset: Int): [User!]! }
graphql
# mutations/index.graphql extend type Mutation { createUser(input: CreateUserInput!): User! updateUser(id: ID!, input: UpdateUserInput!): User! deleteUser(id: ID!): Boolean! }

2. 代码规范

命名约定

typescript
// 类型定义使用 PascalCase type User { id: ID! name: String! } // 字段使用 camelCase type User { firstName: String! lastName: String! } // 输入类型以 Input 结尾 input CreateUserInput { name: String! email: String! } // 枚举使用 PascalCase enum UserRole { ADMIN USER GUEST }

Resolver 命名

typescript
// Resolver 文件命名: *.resolver.ts // user.resolver.ts export const userResolvers = { Query: { user: () => {}, users: () => {} }, Mutation: { createUser: () => {}, updateUser: () => {}, deleteUser: () => {} }, User: { posts: () => {}, comments: () => {} } };

3. 错误处理

统一错误格式

typescript
class GraphQLError extends Error { constructor( public message: string, public code: string, public extensions?: Record<string, any> ) { super(message); this.name = 'GraphQLError'; } } class ValidationError extends GraphQLError { constructor(message: string, public field?: string) { super(message, 'VALIDATION_ERROR', { field }); } } class NotFoundError extends GraphQLError { constructor(resource: string, id: string) { super(`${resource} with id ${id} not found`, 'NOT_FOUND'); } } class AuthenticationError extends GraphQLError { constructor(message = 'Authentication required') { super(message, 'AUTHENTICATION_ERROR'); } } class AuthorizationError extends GraphQLError { constructor(message = 'Not authorized') { super(message, 'AUTHORIZATION_ERROR'); } }

错误处理中间件

typescript
const formatError = (error: any) => { if (error instanceof GraphQLError) { return { message: error.message, code: error.code, extensions: error.extensions }; } // 生产环境不暴露详细错误 if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' }; } return error; };

4. 日志记录

结构化日志

typescript
import winston from 'winston'; const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: 'combined.log' }) ] }); // 在 Resolver 中使用 export const resolvers = { Query: { user: async (_, { id }, context) => { logger.info('Fetching user', { userId: id }); try { const user = await userService.findById(id); logger.info('User fetched successfully', { userId: id }); return user; } catch (error) { logger.error('Error fetching user', { userId: id, error }); throw error; } } } };

5. 测试策略

单元测试

typescript
import { userResolvers } from './user.resolver'; import { UserService } from '../services/userService'; describe('User Resolvers', () => { describe('Query.user', () => { it('should return user by id', async () => { const mockUser = { id: '1', name: 'John', email: 'john@example.com' }; jest.spyOn(UserService, 'findById').mockResolvedValue(mockUser); const result = await userResolvers.Query.user(null, { id: '1' }); expect(result).toEqual(mockUser); }); it('should throw NotFoundError if user not found', async () => { jest.spyOn(UserService, 'findById').mockResolvedValue(null); await expect( userResolvers.Query.user(null, { id: '1' }) ).rejects.toThrow('User with id 1 not found'); }); }); });

集成测试

typescript
import { ApolloServer } from 'apollo-server'; import { createTestClient } from 'apollo-server-testing'; import { typeDefs, resolvers } from './schema'; describe('GraphQL API Integration Tests', () => { const server = new ApolloServer({ typeDefs, resolvers }); const { query, mutate } = createTestClient(server); describe('Query.users', () => { it('should return all users', async () => { const GET_USERS = ` query GetUsers { users { id name email } } `; const { data, errors } = await query(GET_USERS); expect(errors).toBeUndefined(); expect(data.users).toBeDefined(); expect(Array.isArray(data.users)).toBe(true); }); }); describe('Mutation.createUser', () => { it('should create a new user', async () => { const CREATE_USER = ` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email } } `; const variables = { input: { name: 'John Doe', email: 'john@example.com' } }; const { data, errors } = await mutate(CREATE_USER, { variables }); expect(errors).toBeUndefined(); expect(data.createUser).toBeDefined(); expect(data.createUser.name).toBe('John Doe'); }); }); });

6. 文档生成

使用 GraphQL Code Generator

typescript
// codegen.yml schema: ./src/graphql/schema/**/*.graphql documents: ./src/graphql/documents/**/*.graphql generates: ./src/generated/graphql.ts: plugins: - typescript - typescript-resolvers config: contextType: ./context#Context scalars: DateTime: Date

自动生成文档

typescript
import { ApolloServer } from 'apollo-server'; const server = new ApolloServer({ typeDefs, resolvers, introspection: true, // 启用内省 playground: true, // 启用 Playground plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { // 记录查询信息 console.log('Operation:', context.request.operationName); console.log('Variables:', context.request.variables); } }) } ] });

7. 性能监控

使用 Apollo Studio

typescript
import { ApolloServerPluginUsageReporting } from 'apollo-server-core'; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ apiKey: process.env.APOLLO_KEY, graphRef: 'my-graph@current' }) ] });

自定义监控

typescript
import { promisify } from 'util'; const metrics = { queryDuration: new Map<string, number[]>(), queryCount: new Map<string, number>() }; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { const operationName = context.request.operationName || 'anonymous'; const duration = context.metrics.duration; if (!metrics.queryDuration.has(operationName)) { metrics.queryDuration.set(operationName, []); } metrics.queryDuration.get(operationName)!.push(duration); metrics.queryCount.set( operationName, (metrics.queryCount.get(operationName) || 0) + 1 ); } }) } ] });

8. 部署策略

Docker 化

dockerfile
# Dockerfile FROM node:18-alpine WORKDIR /app COPY package*.json ./ RUN npm ci --only=production COPY . . RUN npm run build EXPOSE 4000 CMD ["npm", "start"]
yaml
# docker-compose.yml version: '3.8' services: graphql: build: . ports: - "4000:4000" environment: - NODE_ENV=production - DATABASE_URL=postgresql://user:pass@db:5432/graphql depends_on: - db - redis db: image: postgres:14 environment: - POSTGRES_DB=graphql - POSTGRES_USER=user - POSTGRES_PASSWORD=pass redis: image: redis:7

CI/CD 流程

yaml
# .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 18 - run: npm ci - run: npm run lint - run: npm run test - run: npm run build deploy: needs: test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v3 - uses: docker/login-action@v2 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - run: docker build -t ghcr.io/${{ github.repository }}:latest . - run: docker push ghcr.io/${{ github.repository }}:latest

9. 版本控制

Schema 演进策略

graphql
# 添加新字段 - 向后兼容 type User { id: ID! name: String! email: String! # 新增字段 phoneNumber: String } # 废弃字段 - 提供替代方案 type User { id: ID! name: String! # 废弃字段 fullName: String @deprecated(reason: "Use 'name' instead") email: String! } # 修改类型 - 需要谨慎 # 不好的做法 type User { id: ID! age: String # 从 Int 改为 String } # 好的做法 - 添加新字段,逐步迁移 type User { id: ID! age: Int ageString: String @deprecated(reason: "Use 'age' instead") }

10. 团队协作

Schema 评审流程

  1. 设计阶段: 团队讨论 Schema 设计
  2. 文档阶段: 编写详细的 Schema 文档
  3. 评审阶段: 团队成员评审 Schema
  4. 实现阶段: 实现 Resolver 和业务逻辑
  5. 测试阶段: 编写测试用例
  6. 部署阶段: 部署到测试环境
  7. 监控阶段: 监控 API 性能和错误

代码审查清单

  • Schema 设计合理
  • 命名符合规范
  • 错误处理完善
  • 性能优化到位
  • 安全措施完备
  • 测试覆盖充分
  • 文档完整准确
  • 日志记录合理

11. 最佳实践总结

方面最佳实践
项目结构模块化、分层清晰
代码规范统一命名约定、代码风格
错误处理统一错误格式、详细错误信息
日志记录结构化日志、关键操作记录
测试策略单元测试、集成测试、E2E 测试
文档生成自动生成、保持更新
性能监控实时监控、性能分析
部署策略容器化、CI/CD 自动化
版本控制向后兼容、渐进式演进
团队协作代码审查、知识共享
标签:GraphQL