Skip to main contentSkip to navigationSkip to search
API Design Principles: Building Scalable REST and GraphQL APIs

API Design Principles: Building Scalable REST and GraphQL APIs

17 min read

The API Design Challenge: Beyond CRUD Operations

When we started building LitReview-AI, I thought API design was straightforward: expose data, handle requests, return responses. Six months and 50+ endpoints later, I realized API design is a discipline that can make or break an application's success.

Our API evolved from simple CRUD to a complex system handling AI analysis, real-time collaboration, and multi-user workflows. Here's what we learned about building APIs that scale, maintain themselves, and provide excellent developer experience.


REST vs GraphQL: The Architectural Decision

Initial Approach: REST-First Design

We started with RESTful APIs because they're familiar and well-documented:

// ❌ Our initial REST approach - lots of endpoints
// GET /api/users/:id
// GET /api/users/:id/analyses
// GET /api/analyses/:id
// GET /api/analyses/:id/results
// POST /api/analyses
// PUT /api/analyses/:id
// DELETE /api/analyses/:id

// Problems we encountered:
// 1. Over-fetching data (getting user data when we only need email)
// 2. Under-fetching data (multiple API calls for related data)
// 3. Version management complexity
// 4. Client-side data composition logic

The GraphQL Migration: When and Why

After hitting performance issues, we migrated to GraphQL:

// ✅ GraphQL schema - single endpoint, flexible queries
const typeDefs = gql`
  type User {
    id: ID!
    email: String!
    profile: UserProfile!
    analyses(first: Int = 10, after: String): AnalysisConnection!
  }

  type Analysis {
    id: ID!
    title: String!
    content: String!
    user: User!
    results: [AnalysisResult!]!
    createdAt: DateTime!
  }

  type Query {
    user(id: ID!): User
    analyses(filter: AnalysisFilter, sort: AnalysisSort): [Analysis!]!
  }

  type Mutation {
    createAnalysis(input: CreateAnalysisInput!): Analysis!
    updateAnalysis(id: ID!, input: UpdateAnalysisInput!): Analysis!
  }
`;

// Benefits:
// 1. Single endpoint - no version management issues
// 2. Clients request exactly what they need
// 3. Strong typing with schema validation
// 4. Real-time subscriptions built-in

Decision Framework: Use GraphQL when:

  • Multiple client types with different data needs
  • Complex relationships between entities
  • Real-time data requirements
  • Rapid development cycles

Stick with REST when:

  • Simple CRUD operations
  • Public APIs with predictable usage patterns
  • File upload/download requirements
  • Legacy system integration

Core API Design Principles

1. Consistency is King

Consistent naming and structure reduce cognitive load:

// ✅ Consistent naming conventions
interface ApiResponse<T> {
  readonly data: T;
  readonly meta: {
    readonly timestamp: string;
    readonly requestId: string;
    readonly version: string;
  };
}

interface PaginatedResponse<T> extends ApiResponse<T[]> {
  readonly pagination: {
    readonly page: number;
    readonly limit: number;
    readonly total: number;
    readonly hasNext: boolean;
    readonly hasPrev: boolean;
  };
}

// Consistent error response structure
interface ApiError {
  readonly code: string;
  readonly message: string;
  readonly details?: Record<string, unknown>;
  readonly timestamp: string;
  readonly requestId: string;
}

2. Resource-Oriented Design

Design around business concepts, not database tables:

// ❌ Database-oriented design
interface UserTable {
  id: number;
  email: string;
  password_hash: string;
  created_at: string;
}

// ✅ Business-oriented design
interface User {
  readonly id: string;
  readonly email: string;
  readonly profile: UserProfile;
  readonly preferences: UserPreferences;
  readonly membership: MembershipInfo;
}

interface UserProfile {
  readonly firstName: string;
  readonly lastName: string;
  readonly institution?: string;
  readonly researchFields: readonly string[];
  readonly avatar?: string;
}

3. Proper HTTP Status Codes

Use status codes semantically:

// ✅ Semantic status code usage
class ApiResponseHandler {
  // 200 OK - Successful GET, PUT, DELETE
  success<T>(data: T): Response {
    return Response.json({
      data,
      meta: this.buildMeta(),
    });
  }

  // 201 Created - Successful POST
  created<T>(data: T, location: string): Response {
    return new Response(JSON.stringify({
      data,
      meta: this.buildMeta(),
    }), {
      status: 201,
      headers: { 'Location': location },
    });
  }

  // 204 No Content - Successful DELETE with no body
  noContent(): Response {
    return new Response(null, { status: 204 });
  }

  // 400 Bad Request - Client validation errors
  badRequest(errors: ValidationError[]): Response {
    return Response.json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Request validation failed',
        details: { errors },
      },
      meta: this.buildMeta(),
    }, { status: 400 });
  }

  // 401 Unauthorized - Authentication required
  unauthorized(): Response {
    return Response.json({
      error: {
        code: 'UNAUTHORIZED',
        message: 'Authentication required',
      },
      meta: this.buildMeta(),
    }, { status: 401 });
  }

  // 403 Forbidden - Insufficient permissions
  forbidden(resource: string): Response {
    return Response.json({
      error: {
        code: 'FORBIDDEN',
        message: `Insufficient permissions for ${resource}`,
      },
      meta: this.buildMeta(),
    }, { status: 403 });
  }

  // 404 Not Found - Resource doesn't exist
  notFound(resource: string): Response {
    return Response.json({
      error: {
        code: 'NOT_FOUND',
        message: `${resource} not found`,
      },
      meta: this.buildMeta(),
    }, { status: 404 });
  }

  // 409 Conflict - Resource state conflict
  conflict(details: Record<string, unknown>): Response {
    return Response.json({
      error: {
        code: 'CONFLICT',
        message: 'Resource state conflict',
        details,
      },
      meta: this.buildMeta(),
    }, { status: 409 });
  }

  // 422 Unprocessable Entity - Semantic validation errors
  unprocessableEntity(errors: Record<string, string[]>): Response {
    return Response.json({
      error: {
        code: 'UNPROCESSABLE_ENTITY',
        message: 'Semantic validation failed',
        details: { errors },
      },
      meta: this.buildMeta(),
    }, { status: 422 });
  }

  // 500 Internal Server Error - Unexpected server errors
  internalServerError(error: Error): Response {
    console.error('Internal server error:', error);
    return Response.json({
      error: {
        code: 'INTERNAL_SERVER_ERROR',
        message: 'An unexpected error occurred',
      },
      meta: this.buildMeta(),
    }, { status: 500 });
  }

  private buildMeta() {
    return {
      timestamp: new Date().toISOString(),
      requestId: crypto.randomUUID(),
      version: 'v1',
    };
  }
}

Advanced API Patterns

1. Optimistic Updates with Conflict Resolution

// ✅ Optimistic update pattern
interface UpdateRequest<T> {
  readonly id: string;
  readonly data: Partial<T>;
  readonly expectedVersion: number;
}

interface UpdateResponse<T> {
  readonly data: T;
  readonly conflict?: {
    readonly currentData: T;
    readonly conflicts: string[];
  };
}

class OptimisticUpdateHandler<T> {
  async update(request: UpdateRequest<T>): Promise<UpdateResponse<T>> {
    // 1. Get current version
    const current = await this.repository.findById(request.id);
    if (!current) {
      throw new NotFoundError(`Resource ${request.id} not found`);
    }

    // 2. Check for conflicts
    if (current.version !== request.expectedVersion) {
      const conflicts = this.detectConflicts(request.data, current);
      return {
        data: current,
        conflict: {
          currentData: current,
          conflicts,
        },
      };
    }

    // 3. Apply update
    const updated = await this.repository.update(request.id, {
      ...request.data,
      version: current.version + 1,
    });

    return { data: updated };
  }

  private detectConflicts(updates: Partial<T>, current: T): string[] {
    const conflicts: string[] = [];

    for (const [key, value] of Object.entries(updates)) {
      if (current[key as keyof T] !== value) {
        conflicts.push(key);
      }
    }

    return conflicts;
  }
}

2. Rate Limiting with Intelligent Bypass

// ✅ Smart rate limiting
interface RateLimitConfig {
  readonly windowMs: number;
  readonly maxRequests: number;
  readonly skipSuccessfulRequests: boolean;
  readonly keyGenerator: (req: Request) => string;
}

class SmartRateLimiter {
  private readonly clients = new Map<string, {
    count: number;
    resetTime: number;
    lastRequestTime: number;
  }>();

  constructor(private config: RateLimitConfig) {}

  async checkLimit(req: Request): Promise<{
    allowed: boolean;
    remaining: number;
    resetTime: number;
  }> {
    const key = this.config.keyGenerator(req);
    const now = Date.now();
    const client = this.clients.get(key) || {
      count: 0,
      resetTime: now + this.config.windowMs,
      lastRequestTime: now,
    };

    // Reset window if expired
    if (now > client.resetTime) {
      client.count = 0;
      client.resetTime = now + this.config.windowMs;
    }

    const allowed = client.count < this.config.maxRequests;

    if (allowed) {
      client.count++;
    }

    this.clients.set(key, client);

    return {
      allowed,
      remaining: Math.max(0, this.config.maxRequests - client.count),
      resetTime: client.resetTime,
    };
  }

  async recordSuccess(key: string): Promise<void> {
    if (this.config.skipSuccessfulRequests) {
      const client = this.clients.get(key);
      if (client && client.count > 0) {
        client.count--;
      }
    }
  }
}

3. Caching Strategy with Cache Invalidation

// ✅ Intelligent caching with cache tags
interface CacheConfig {
  readonly ttl: number;
  readonly tags: string[];
  readonly invalidateOnChange: boolean;
}

interface CacheEntry<T> {
  readonly data: T;
  readonly tags: string[];
  readonly expiresAt: number;
  readonly createdAt: number;
}

class IntelligentCache {
  private readonly cache = new Map<string, CacheEntry<unknown>>();
  private readonly tagIndex = new Map<string, Set<string>>();

  async get<T>(key: string): Promise<T | null> {
    const entry = this.cache.get(key);
    if (!entry) return null;

    if (Date.now() > entry.expiresAt) {
      this.delete(key);
      return null;
    }

    return entry.data as T;
  }

  async set<T>(key: string, data: T, config: CacheConfig): Promise<void> {
    const entry: CacheEntry<T> = {
      data,
      tags: config.tags,
      expiresAt: Date.now() + config.ttl,
      createdAt: Date.now(),
    };

    this.cache.set(key, entry);

    // Update tag index
    config.tags.forEach(tag => {
      if (!this.tagIndex.has(tag)) {
        this.tagIndex.set(tag, new Set());
      }
      this.tagIndex.get(tag)!.add(key);
    });
  }

  invalidateByTag(tag: string): void {
    const keys = this.tagIndex.get(tag);
    if (keys) {
      keys.forEach(key => this.delete(key));
      this.tagIndex.delete(tag);
    }
  }

  invalidateByPattern(pattern: RegExp): void {
    for (const key of this.cache.keys()) {
      if (pattern.test(key)) {
        this.delete(key);
      }
    }
  }

  private delete(key: string): void {
    const entry = this.cache.get(key);
    if (entry) {
      entry.tags.forEach(tag => {
        const keys = this.tagIndex.get(tag);
        if (keys) {
          keys.delete(key);
          if (keys.size === 0) {
            this.tagIndex.delete(tag);
          }
        }
      });
      this.cache.delete(key);
    }
  }
}

Authentication and Authorization Patterns

1. JWT with Refresh Token Strategy

// ✅ Secure JWT implementation
interface TokenPair {
  readonly accessToken: string;
  readonly refreshToken: string;
  readonly expiresIn: number;
}

interface JWTPayload {
  readonly sub: string;
  readonly email: string;
  readonly role: string;
  readonly permissions: string[];
  readonly iat: number;
  readonly exp: number;
}

class JWTService {
  constructor(
    private readonly accessSecret: string,
    private readonly refreshSecret: string,
    private readonly accessExpiry: number = 15 * 60, // 15 minutes
    private readonly refreshExpiry: number = 7 * 24 * 60 * 60 // 7 days
  ) {}

  generateTokenPair(user: User): TokenPair {
    const now = Math.floor(Date.now() / 1000);

    const payload: JWTPayload = {
      sub: user.id,
      email: user.email,
      role: user.role,
      permissions: user.permissions,
      iat: now,
      exp: now + this.accessExpiry,
    };

    const accessToken = jwt.sign(payload, this.accessSecret, {
      algorithm: 'HS256',
    });

    const refreshToken = jwt.sign(
      { sub: user.id, type: 'refresh' },
      this.refreshSecret,
      {
        algorithm: 'HS256',
        expiresIn: this.refreshExpiry,
      }
    );

    return {
      accessToken,
      refreshToken,
      expiresIn: this.accessExpiry,
    };
  }

  verifyAccessToken(token: string): JWTPayload {
    try {
      return jwt.verify(token, this.accessSecret) as JWTPayload;
    } catch (error) {
      throw new UnauthorizedError('Invalid access token');
    }
  }

  refreshAccessToken(refreshToken: string): Promise<TokenPair> {
    return new Promise((resolve, reject) => {
      jwt.verify(refreshToken, this.refreshSecret, (err, decoded) => {
        if (err) {
          reject(new UnauthorizedError('Invalid refresh token'));
          return;
        }

        const { sub } = decoded as { sub: string };
        // Fetch user from database
        this.userService.findById(sub)
          .then(user => {
            if (!user) {
              reject(new UnauthorizedError('User not found'));
              return;
            }
            resolve(this.generateTokenPair(user));
          })
          .catch(reject);
      });
    });
  }
}

2. Role-Based Access Control (RBAC)

// ✅ Flexible RBAC system
interface Permission {
  readonly resource: string;
  readonly action: string;
  readonly conditions?: Record<string, unknown>;
}

interface Role {
  readonly name: string;
  readonly permissions: readonly Permission[];
  readonly inherits?: readonly string[];
}

class AuthorizationService {
  private readonly roles = new Map<string, Role>();

  constructor(roleDefinitions: Role[]) {
    roleDefinitions.forEach(role => {
      this.roles.set(role.name, role);
    });
  }

  hasPermission(
    userRole: string,
    resource: string,
    action: string,
    context?: Record<string, unknown>
  ): boolean {
    const role = this.roles.get(userRole);
    if (!role) return false;

    // Check direct permissions
    const hasDirectPermission = role.permissions.some(permission =>
      this.matchesPermission(permission, resource, action, context)
    );

    if (hasDirectPermission) return true;

    // Check inherited permissions
    if (role.inherits) {
      return role.inherits.some(inheritedRole =>
        this.hasPermission(inheritedRole, resource, action, context)
      );
    }

    return false;
  }

  private matchesPermission(
    permission: Permission,
    resource: string,
    action: string,
    context?: Record<string, unknown>
  ): boolean {
    // Exact match
    if (permission.resource === resource && permission.action === action) {
      return true;
    }

    // Wildcard match
    if (permission.resource === '*' && permission.action === action) {
      return true;
    }

    if (permission.resource === resource && permission.action === '*') {
      return true;
    }

    // Conditional match
    if (permission.conditions && context) {
      return this.evaluateConditions(permission.conditions, context);
    }

    return false;
  }

  private evaluateConditions(
    conditions: Record<string, unknown>,
    context: Record<string, unknown>
  ): boolean {
    return Object.entries(conditions).every(([key, value]) => {
      return context[key] === value;
    });
  }
}

API Documentation and Developer Experience

1. OpenAPI Specification with Examples

// ✅ Comprehensive API documentation
const openApiSpec = {
  openapi: '3.0.0',
  info: {
    title: 'LitReview AI API',
    version: '1.0.0',
    description: 'AI-powered literature review API',
  },
  paths: {
    '/api/analyses': {
      get: {
        summary: 'List analyses',
        parameters: [
          {
            name: 'page',
            in: 'query',
            schema: { type: 'integer', minimum: 1, default: 1 },
          },
          {
            name: 'limit',
            in: 'query',
            schema: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
          },
          {
            name: 'search',
            in: 'query',
            schema: { type: 'string' },
          },
        ],
        responses: {
          200: {
            description: 'Successful response',
            content: {
              'application/json': {
                schema: { $ref: '#/components/schemas/PaginatedAnalyses' },
                examples: {
                  'default': {
                    summary: 'Default example',
                    value: {
                      data: [
                        {
                          id: '123e4567-e89b-12d3-a456-426614174000',
                          title: 'AI in Healthcare',
                          content: '...',
                          user: {
                            id: 'user-123',
                            email: 'researcher@university.edu',
                          },
                          createdAt: '2025-01-11T10:00:00Z',
                        },
                      ],
                      pagination: {
                        page: 1,
                        limit: 10,
                        total: 25,
                        hasNext: true,
                        hasPrev: false,
                      },
                    },
                  },
                },
              },
            },
          },
        },
      },
    },
  },
  components: {
    schemas: {
      Analysis: {
        type: 'object',
        required: ['id', 'title', 'content', 'user'],
        properties: {
          id: { type: 'string', format: 'uuid' },
          title: { type: 'string', maxLength: 200 },
          content: { type: 'string' },
          user: { $ref: '#/components/schemas/User' },
          createdAt: { type: 'string', format: 'date-time' },
        },
      },
    },
  },
};

2. Interactive API Documentation

// ✅ Interactive documentation setup
import { setupSwaggerUi } from 'next-swagger-doc';

const swaggerUiConfig = setupSwaggerUi({
  title: 'LitReview AI API',
  version: '1.0.0',
  specUrl: '/api/swagger.json',
  swaggerOptions: {
    supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
    docExpansion: 'none',
    defaultModelsExpandDepth: 2,
    deepLinking: true,
    displayRequestDuration: true,
    filter: true,
    showExtensions: true,
    showCommonExtensions: true,
  },
});

Performance Optimization Strategies

1. Response Compression and Optimization

// ✅ Response optimization middleware
function createOptimizedResponse(data: unknown, request: Request): Response {
  // Check if client accepts compression
  const acceptEncoding = request.headers.get('accept-encoding') || '';
  const supportsGzip = acceptEncoding.includes('gzip');
  const supportsBrotli = acceptEncoding.includes('br');

  // Serialize data
  const serialized = JSON.stringify(data);

  // Compress if supported
  if (supportsBrotli) {
    const compressed = compress(serialized, 'br');
    return new Response(compressed, {
      headers: {
        'Content-Type': 'application/json',
        'Content-Encoding': 'br',
        'Content-Length': compressed.length.toString(),
      },
    });
  }

  if (supportsGzip) {
    const compressed = compress(serialized, 'gzip');
    return new Response(compressed, {
      headers: {
        'Content-Type': 'application/json',
        'Content-Encoding': 'gzip',
        'Content-Length': compressed.length.toString(),
      },
    });
  }

  return new Response(serialized, {
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': serialized.length.toString(),
    },
  });
}

2. Database Query Optimization

// ✅ Optimized database queries
class AnalysisRepository {
  async findManyOptimized(options: {
    page: number;
    limit: number;
    filters?: AnalysisFilters;
    sort?: AnalysisSort;
  }): Promise<PaginatedResponse<Analysis>> {
    // Build query with proper indexing
    let query = this.supabase
      .from('analyses')
      .select(`
        id,
        title,
        content,
        created_at,
        updated_at,
        user:users(id, email, profile),
        analysis_results(id, score, data)
      `, { count: 'exact' });

    // Apply filters with indexed columns
    if (options.filters) {
      if (options.filters.userId) {
        query = query.eq('user_id', options.filters.userId);
      }
      if (options.filters.status) {
        query = query.eq('status', options.filters.status);
      }
      if (options.filters.dateRange) {
        query = query
          .gte('created_at', options.filters.dateRange.start)
          .lte('created_at', options.filters.dateRange.end);
      }
    }

    // Apply sorting
    if (options.sort) {
      query = query.order(options.sort.field, {
        ascending: options.sort.direction === 'asc',
      });
    }

    // Apply pagination
    const offset = (options.page - 1) * options.limit;
    query = query.range(offset, offset + options.limit - 1);

    const { data, error, count } = await query;

    if (error) throw new DatabaseError(error.message);

    return {
      data: data || [],
      pagination: {
        page: options.page,
        limit: options.limit,
        total: count || 0,
        hasNext: offset + options.limit < (count || 0),
        hasPrev: options.page > 1,
      },
    };
  }
}

Testing Strategies

1. Contract Testing

// ✅ API contract testing
describe('API Contract Tests', () => {
  describe('POST /api/analyses', () => {
    it('should create analysis with valid data', async () => {
      const validInput = {
        title: 'Test Analysis',
        content: 'Test content',
        tags: ['test', 'api'],
      };

      const response = await request(app)
        .post('/api/analyses')
        .set('Authorization', `Bearer ${validToken}`)
        .send(validInput)
        .expect(201);

      expect(response.body).toMatchObject({
        data: {
          id: expect.any(String),
          title: validInput.title,
          content: validInput.content,
          tags: validInput.tags,
          createdAt: expect.any(String),
        },
        meta: {
          timestamp: expect.any(String),
          requestId: expect.any(String),
          version: 'v1',
        },
      });
    });

    it('should return 400 for invalid data', async () => {
      const invalidInput = {
        title: '', // Empty title
        content: 'x'.repeat(10001), // Too long
      };

      const response = await request(app)
        .post('/api/analyses')
        .set('Authorization', `Bearer ${validToken}`)
        .send(invalidInput)
        .expect(400);

      expect(response.body).toMatchObject({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Request validation failed',
          details: {
            errors: expect.arrayContaining([
              expect.objectContaining({
                field: 'title',
                message: expect.any(String),
              }),
            ]),
          },
        },
      });
    });
  });
});

2. Load Testing

// ✅ Load testing setup
import { load } from 'cheerio';
import { performance } from 'perf_hooks';

class LoadTester {
  constructor(
    private readonly baseUrl: string,
    private readonly concurrency: number = 10,
    private readonly duration: number = 30000 // 30 seconds
  ) {}

  async runLoadTest(endpoint: string): Promise<LoadTestResults> {
    const startTime = performance.now();
    const results: RequestResult[] = [];
    const activeRequests: Promise<void>[] = [];

    const testLoop = async () => {
      while (performance.now() - startTime < this.duration) {
        if (activeRequests.length < this.concurrency) {
          const request = this.makeRequest(endpoint);
          activeRequests.push(request);

          request.then((result) => {
            results.push(result);
            activeRequests.splice(activeRequests.indexOf(request), 1);
          });
        }

        await new Promise(resolve => setTimeout(resolve, 100));
      }
    };

    await testLoop();
    await Promise.all(activeRequests);

    return this.analyzeResults(results);
  }

  private async makeRequest(endpoint: string): Promise<RequestResult> {
    const startTime = performance.now();

    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`, {
        method: 'GET',
        headers: {
          'Authorization': `Bearer ${process.env.TEST_TOKEN}`,
        },
      });

      const endTime = performance.now();
      const responseTime = endTime - startTime;

      return {
        success: response.ok,
        statusCode: response.status,
        responseTime,
        timestamp: new Date().toISOString(),
      };
    } catch (error) {
      const endTime = performance.now();
      const responseTime = endTime - startTime;

      return {
        success: false,
        statusCode: 0,
        responseTime,
        timestamp: new Date().toISOString(),
        error: error instanceof Error ? error.message : 'Unknown error',
      };
    }
  }

  private analyzeResults(results: RequestResult[]): LoadTestResults {
    const successful = results.filter(r => r.success);
    const failed = results.filter(r => !r.success);

    const responseTimes = successful.map(r => r.responseTime);
    const avgResponseTime = responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length;
    const maxResponseTime = Math.max(...responseTimes);
    const minResponseTime = Math.min(...responseTimes);

    return {
      totalRequests: results.length,
      successfulRequests: successful.length,
      failedRequests: failed.length,
      successRate: (successful.length / results.length) * 100,
      averageResponseTime: avgResponseTime,
      maxResponseTime,
      minResponseTime,
      requestsPerSecond: results.length / (this.duration / 1000),
    };
  }
}

interface RequestResult {
  success: boolean;
  statusCode: number;
  responseTime: number;
  timestamp: string;
  error?: string;
}

interface LoadTestResults {
  totalRequests: number;
  successfulRequests: number;
  failedRequests: number;
  successRate: number;
  averageResponseTime: number;
  maxResponseTime: number;
  minResponseTime: number;
  requestsPerSecond: number;
}

Common Pitfalls and Solutions

Pitfall 1: Inconsistent Error Handling

// ❌ Inconsistent error responses
// Endpoint 1 returns: { error: "Invalid input" }
// Endpoint 2 returns: { message: "Bad request", details: [...] }
// Endpoint 3 returns: { success: false, error: "Validation failed" }

// ✅ Consistent error handling
class ErrorHandler {
  static handle(error: unknown): Response {
    if (error instanceof ValidationError) {
      return Response.json({
        error: {
          code: 'VALIDATION_ERROR',
          message: error.message,
          details: { field: error.field, value: error.value },
        },
        meta: this.buildMeta(),
      }, { status: 400 });
    }

    if (error instanceof DatabaseError) {
      return Response.json({
        error: {
          code: 'DATABASE_ERROR',
          message: 'Database operation failed',
        },
        meta: this.buildMeta(),
      }, { status: 500 });
    }

    return Response.json({
      error: {
        code: 'INTERNAL_ERROR',
        message: 'An unexpected error occurred',
      },
      meta: this.buildMeta(),
    }, { status: 500 });
  }
}

Pitfall 2: Missing Input Validation

// ❌ No validation
app.post('/api/users', async (req, res) => {
  const user = req.body;
  // Direct use of unvalidated input
  const result = await userService.create(user);
  res.json({ data: result });
});

// ✅ Comprehensive validation
app.post('/api/users',
  body('user').isObject().withMessage('User must be an object'),
  body('user.email').isEmail().normalizeEmail(),
  body('user.name').isLength({ min: 1, max: 100 }).trim(),
  body('user.age').isInt({ min: 13, max: 120 }),
  async (req, res) => {
    try {
      const result = await userService.create(req.body);
      res.status(201).json({ data: result });
    } catch (error) {
      ErrorHandler.handle(error, res);
    }
  }
);

Conclusion: Building APIs That Last

API design is not just about endpoints and data structures—it's about creating a foundation that supports growth, maintains security, and provides excellent developer experience.

Our journey taught us several key lessons:

  1. Start with the contract: Design your API schema before implementation
  2. Choose the right paradigm: REST for simplicity, GraphQL for flexibility
  3. Invest in documentation: Good documentation is as important as good code
  4. Test comprehensively: Contract tests, load tests, and integration tests
  5. Monitor and iterate: Use metrics to guide API improvements

💡 Final Advice: An API is a product. Treat it as such—document it, version it, support it, and continuously improve it based on user feedback.


This article represents lessons learned from building and maintaining production APIs serving thousands of users, handling complex AI workflows, and scaling to meet enterprise requirements. All patterns have been tested in real-world scenarios with measurable improvements in performance and developer experience.

相关文章

11 min

TypeScript Best Practices for Enterprise Applications: Lessons from 50,000+ Lines of Code

TypeScriptEnterprise
阅读全文
10 min

Microservices Architecture: Scalability Patterns That Reduced Deployment Time by 90%

MicroservicesArchitecture
阅读全文
11 min

Security Best Practices: Protecting Modern Web Applications in 2025

SecurityAuthentication
阅读全文