Back to patterns

API Integration

INTEGRATION

A comprehensive debugging flow for API integrations, focusing on request/response cycles, error handling patterns, and timeout/retry strategies.

Diagram for API Integration

Use Cases

  • Complex API integrations
  • Error handling strategies
  • Request/response debugging
  • Timeout and retry patterns

Production Implementation

Implementation
// Real-world example: Robust API client with retries, caching, and error handling
interface RequestConfig {
  baseURL: string;
  timeout?: number;
  retries?: number;
  cacheTTL?: number;
}

interface APIResponse<T> {
  data: T;
  status: number;
  headers: Record<string, string>;
}

interface APIError extends Error {
  status?: number;
  code?: string;
  response?: any;
}

class APIClient {
  private baseURL: string;
  private timeout: number;
  private retries: number;
  private cache: Map<string, { data: any; timestamp: number }>;
  private cacheTTL: number;
  private logger: Console;

  constructor(config: RequestConfig) {
    this.baseURL = config.baseURL;
    this.timeout = config.timeout || 5000;
    this.retries = config.retries || 3;
    this.cacheTTL = config.cacheTTL || 300000; // 5 minutes
    this.cache = new Map();
    this.logger = console;
  }

  async get<T>(path: string, options: {
    useCache?: boolean;
    headers?: Record<string, string>;
  } = {}): Promise<APIResponse<T>> {
    const cacheKey = `GET:${path}`;
    
    // Check cache if enabled
    if (options.useCache) {
      const cached = this.getFromCache<T>(cacheKey);
      if (cached) return cached;
    }

    try {
      const response = await this.executeWithRetry<T>('GET', path, null, options.headers);
      
      // Cache successful responses
      if (options.useCache) {
        this.setInCache(cacheKey, response);
      }

      return response;
    } catch (error) {
      this.handleError(error, 'GET', path);
      throw error;
    }
  }

  async post<T>(
    path: string,
    data: any,
    headers?: Record<string, string>
  ): Promise<APIResponse<T>> {
    try {
      return await this.executeWithRetry<T>('POST', path, data, headers);
    } catch (error) {
      this.handleError(error, 'POST', path);
      throw error;
    }
  }

  private async executeWithRetry<T>(
    method: string,
    path: string,
    data?: any,
    headers?: Record<string, string>,
    attempt: number = 1
  ): Promise<APIResponse<T>> {
    try {
      const controller = new AbortController();
      const timeoutId = setTimeout(() => controller.abort(), this.timeout);

      const response = await fetch(`${this.baseURL}${path}`, {
        method,
        headers: {
          'Content-Type': 'application/json',
          ...headers
        },
        body: data ? JSON.stringify(data) : undefined,
        signal: controller.signal
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        throw this.createAPIError(response);
      }

      const responseData = await response.json();
      
      return {
        data: responseData,
        status: response.status,
        headers: Object.fromEntries(response.headers.entries())
      };

    } catch (error) {
      if (error instanceof Error && error.name === 'AbortError') {
        throw new Error(`Request timeout after ${this.timeout}ms`);
      }

      if (this.shouldRetry(error) && attempt < this.retries) {
        const backoffDelay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
        
        this.logger.warn(`Retrying ${method} ${path} after ${backoffDelay}ms (attempt ${attempt})`);
        
        await new Promise(resolve => setTimeout(resolve, backoffDelay));
        return this.executeWithRetry<T>(method, path, data, headers, attempt + 1);
      }

      throw error;
    }
  }

  private shouldRetry(error: any): boolean {
    // Retry on network errors and 5xx responses
    if (error instanceof Error && error.name === 'TypeError') {
      return true;
    }
    
    return error.status ? error.status >= 500 : false;
  }

  private createAPIError(response: Response): APIError {
    const error: APIError = new Error(`HTTP Error ${response.status}`);
    error.status = response.status;
    error.code = response.statusText;
    return error;
  }

  private getFromCache<T>(key: string): APIResponse<T> | null {
    const cached = this.cache.get(key);
    
    if (!cached) return null;

    if (Date.now() - cached.timestamp > this.cacheTTL) {
      this.cache.delete(key);
      return null;
    }

    return cached.data;
  }

  private setInCache(key: string, data: any): void {
    // Implement LRU if needed
    if (this.cache.size >= 100) {
      const oldestKey = this.cache.keys().next().value;
      this.cache.delete(oldestKey);
    }

    this.cache.set(key, {
      data,
      timestamp: Date.now()
    });
  }

  private handleError(error: any, method: string, path: string): void {
    this.logger.error('API Request Failed', {
      method,
      path,
      error: {
        message: error.message,
        status: error.status,
        code: error.code
      }
    });
  }
}

// Example usage with error handling and retries
async function fetchUserData(userId: string) {
  const api = new APIClient({
    baseURL: 'https://api.example.com',
    timeout: 5000,
    retries: 3
  });

  try {
    // GET with caching
    const user = await api.get(`/users/${userId}`, { useCache: true });
    
    // POST with error handling
    const userPreferences = await api.post(`/users/${userId}/preferences`, {
      theme: 'dark',
      notifications: true
    });

    return {
      user: user.data,
      preferences: userPreferences.data
    };

  } catch (error) {
    if (error instanceof Error) {
      if (error.name === 'AbortError') {
        // Handle timeout
        console.error('Request timed out');
      } else if ((error as APIError).status === 404) {
        // Handle not found
        console.error('User not found');
      } else {
        // Handle other errors
        console.error('Failed to fetch user data:', error.message);
      }
    }
    throw error;
  }
}

Code Examples

Common API Integration Issues

typescript
// ❌ Common API integration issues
async function fetchUserData(userId: string) {
  // Issue 1: No error handling
  const response = await fetch(`/api/users/${userId}`);
  const data = await response.json();
  
  // Issue 2: No timeout handling
  const preferences = await fetch(`/api/users/${userId}/preferences`);
  
  // Issue 3: No retry logic
  if (!response.ok) {
    throw new Error('Request failed');
  }
  
  // Issue 4: No request cancellation
  const longRequest = await fetch('/api/long-operation');
  
  return data;
}

Common issues include missing error handling, no timeout handling, lack of retry logic, and no request cancellation.

Robust API Integration

typescript
// ✅ Robust API integration
async function fetchUserData(userId: string) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 5000);

  try {
    // Parallel requests with timeout
    const [userResponse, preferencesResponse] = await Promise.all([
      fetch(`/api/users/${userId}`, {
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json'
        }
      }),
      fetch(`/api/users/${userId}/preferences`, {
        signal: controller.signal
      })
    ]);

    // Validate responses
    if (!userResponse.ok) {
      throw new Error(`User request failed: ${userResponse.status}`);
    }
    if (!preferencesResponse.ok) {
      throw new Error(`Preferences request failed: ${preferencesResponse.status}`);
    }

    // Parse responses
    const [userData, preferences] = await Promise.all([
      userResponse.json(),
      preferencesResponse.json()
    ]);

    return {
      user: userData,
      preferences
    };

  } catch (error) {
    if (error instanceof Error) {
      if (error.name === 'AbortError') {
        throw new Error('Request timeout');
      }
      // Log error details
      console.error('API request failed:', {
        userId,
        error: error.message,
        timestamp: new Date().toISOString()
      });
    }
    throw error;
    
  } finally {
    clearTimeout(timeout);
  }
}

This implementation includes timeout handling, parallel requests, proper error handling, and request cancellation.

Best Practices

  • Implement proper error handling with specific error types
  • Use timeouts for all requests
  • Implement retry logic with exponential backoff
  • Add request/response logging
  • Use request cancellation
  • Implement caching where appropriate
  • Handle rate limiting and backoff

Common Pitfalls

  • Missing error handling
  • No timeout implementation
  • Lack of retry logic
  • Poor logging practices
  • Missing request cancellation
  • Not handling rate limits
  • Inadequate response validation