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.
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
Related Patterns
Promise Chaining
Visualize and debug complex asynchronous flows and promise chains for structured reasoning and step-by-step task completion.
Memory Leaks 101
A systematic approach to identifying and fixing memory leaks through heap snapshot analysis, memory usage visualization, and common leak pattern detection.