5月28日 09:25
What are the best practices for GraphQL project development
GraphQL Project Development Best Practices
When using GraphQL in actual projects, following best practices can help teams build maintainable, scalable, and high-quality GraphQL APIs. Here are key best practices in project development.
1. Project Structure Organization
Recommended Project Structure
shellgraphql-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
Modular 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. Code Standards
Naming Conventions
typescript// Type definitions use PascalCase type User { id: ID! name: String! } // Fields use camelCase type User { firstName: String! lastName: String! } // Input types end with Input input CreateUserInput { name: String! email: String! } // Enums use PascalCase enum UserRole { ADMIN USER GUEST }
Resolver Naming
typescript// Resolver file naming: *.resolver.ts // user.resolver.ts export const userResolvers = { Query: { user: () => {}, users: () => {} }, Mutation: { createUser: () => {}, updateUser: () => {}, deleteUser: () => {} }, User: { posts: () => {}, comments: () => {} } };
3. Error Handling
Unified Error Format
typescriptclass 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'); } }
Error Handling Middleware
typescriptconst formatError = (error: any) => { if (error instanceof GraphQLError) { return { message: error.message, code: error.code, extensions: error.extensions }; } // Don't expose detailed errors in production if (process.env.NODE_ENV === 'production') { return { message: 'Internal server error', code: 'INTERNAL_SERVER_ERROR' }; } return error; };
4. Logging
Structured Logging
typescriptimport 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' }) ] }); // Use in 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. Testing Strategy
Unit Tests
typescriptimport { 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'); }); }); });
Integration Tests
typescriptimport { 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. Documentation Generation
Using 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
Auto-generate Documentation
typescriptimport { ApolloServer } from 'apollo-server'; const server = new ApolloServer({ typeDefs, resolvers, introspection: true, // Enable introspection playground: true, // Enable Playground plugins: [ { requestDidStart: () => ({ didResolveOperation: (context) => { // Log query information console.log('Operation:', context.request.operationName); console.log('Variables:', context.request.variables); } }) } ] });
7. Performance Monitoring
Using Apollo Studio
typescriptimport { ApolloServerPluginUsageReporting } from 'apollo-server-core'; const server = new ApolloServer({ typeDefs, resolvers, plugins: [ ApolloServerPluginUsageReporting({ apiKey: process.env.APOLLO_KEY, graphRef: 'my-graph@current' }) ] });
Custom Monitoring
typescriptimport { 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. Deployment Strategy
Dockerization
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 Pipeline
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. Version Control
Schema Evolution Strategy
graphql# Add new fields - backward compatible type User { id: ID! name: String! email: String! # New field phoneNumber: String } # Deprecate fields - provide alternative type User { id: ID! name: String! # Deprecated field fullName: String @deprecated(reason: "Use 'name' instead") email: String! } # Modify types - need to be careful # Bad practice type User { id: ID! age: String # Changed from Int to String } # Good practice - add new field, gradual migration type User { id: ID! age: Int ageString: String @deprecated(reason: "Use 'age' instead") }
10. Team Collaboration
Schema Review Process
- Design Phase: Team discusses Schema design
- Documentation Phase: Write detailed Schema documentation
- Review Phase: Team members review Schema
- Implementation Phase: Implement Resolvers and business logic
- Testing Phase: Write test cases
- Deployment Phase: Deploy to test environment
- Monitoring Phase: Monitor API performance and errors
Code Review Checklist
- Schema design is reasonable
- Naming follows conventions
- Error handling is complete
- Performance optimization is in place
- Security measures are complete
- Test coverage is sufficient
- Documentation is complete and accurate
- Logging is reasonable
11. Best Practices Summary
| Aspect | Best Practice |
|---|---|
| Project Structure | Modular, clear layering |
| Code Standards | Unified naming conventions, code style |
| Error Handling | Unified error format, detailed error information |
| Logging | Structured logging, key operation logging |
| Testing Strategy | Unit tests, integration tests, E2E tests |
| Documentation Generation | Auto-generation, keep updated |
| Performance Monitoring | Real-time monitoring, performance analysis |
| Deployment Strategy | Containerization, CI/CD automation |
| Version Control | Backward compatible, progressive evolution |
| Team Collaboration | Code review, knowledge sharing |