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

What are the best practices for GraphQL project development

2月21日 17:00

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

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

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

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'); } }

Error Handling Middleware

typescript
const 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

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' }) ] }); // 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

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'); }); }); });

Integration Tests

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. 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

typescript
import { 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

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

Custom Monitoring

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. 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

  1. Design Phase: Team discusses Schema design
  2. Documentation Phase: Write detailed Schema documentation
  3. Review Phase: Team members review Schema
  4. Implementation Phase: Implement Resolvers and business logic
  5. Testing Phase: Write test cases
  6. Deployment Phase: Deploy to test environment
  7. 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

AspectBest Practice
Project StructureModular, clear layering
Code StandardsUnified naming conventions, code style
Error HandlingUnified error format, detailed error information
LoggingStructured logging, key operation logging
Testing StrategyUnit tests, integration tests, E2E tests
Documentation GenerationAuto-generation, keep updated
Performance MonitoringReal-time monitoring, performance analysis
Deployment StrategyContainerization, CI/CD automation
Version ControlBackward compatible, progressive evolution
Team CollaborationCode review, knowledge sharing
标签:GraphQL