Claude-skill-registry api-client-patterns
HTTP client patterns, API integration, request/response handling, error handling, retry logic, axios usage. Use when building API clients, integrating external services, handling API errors, or making HTTP requests.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/api-client-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-api-client-patterns && rm -rf "$T"
manifest:
skills/data/api-client-patterns/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
API Client Patterns
Core Principles
- Single Responsibility - One client per service
- Centralized Configuration - Base URL, headers, timeouts in one place
- Comprehensive Error Handling - Catch and transform errors appropriately
- Type Safety - Define request/response interfaces
- Retry Logic - Handle transient failures gracefully
API Client Structure
CORRECT: Well-Structured API Client
// client/src/utils/api.js import axios from 'axios'; // Configuration const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3001/api'; const TIMEOUT = 30000; // 30 seconds // Create axios instance with defaults const apiClient = axios.create({ baseURL: API_BASE, timeout: TIMEOUT, headers: { 'Content-Type': 'application/json' } }); // Request interceptor (optional - for auth, logging, etc.) apiClient.interceptors.request.use( (config) => { // Add auth token if available const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error) ); // Response interceptor (optional - for error transformation) apiClient.interceptors.response.use( (response) => response, (error) => { // Transform errors to consistent format const message = error.response?.data?.error || error.message || 'Unknown error'; return Promise.reject(new Error(message)); } ); // API Methods /** * Verify faculty password */ export const verifyPassword = async (password) => { const response = await apiClient.post('/auth/verify', { password }); return response.data; }; /** * Generate or update instructional page */ export const generatePage = async (config, message, history = []) => { const response = await apiClient.post('/generate', { config, message, history }); return response.data; }; /** * Generate AI image using DALL-E */ export const generateImage = async (prompt) => { const response = await apiClient.post('/images/generate', { prompt }); return response.data; }; /** * Upload image to Cloudinary */ export const uploadImage = async (file) => { const formData = new FormData(); formData.append('image', file); const response = await apiClient.post('/images/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' } }); return response.data; }; export default apiClient;
WRONG: Scattered API Calls
// ❌ DON'T DO THIS: Direct axios calls scattered across components import axios from 'axios'; // In Component 1 const handleLogin = async () => { const res = await axios.post('http://localhost:3001/api/auth/verify', { password: pwd }); }; // In Component 2 const handleGenerate = async () => { const res = await axios.post('http://localhost:3001/api/generate', data); }; // ❌ Issues: // - Repeated base URL strings // - No centralized error handling // - No configuration reuse // - Hard to test
Error Handling Patterns
CORRECT: Comprehensive Error Handling
// API client with error transformation export const generatePage = async (config, message, history) => { try { const response = await apiClient.post('/generate', { config, message, history }); return response.data; } catch (error) { // Check different error types if (error.response) { // Server responded with error status const status = error.response.status; const message = error.response.data?.error || 'Server error'; if (status === 400) { throw new Error(`Validation error: ${message}`); } else if (status === 401) { throw new Error('Authentication failed'); } else if (status === 429) { throw new Error('Rate limit exceeded. Please try again later.'); } else if (status >= 500) { throw new Error('Server error. Please try again.'); } else { throw new Error(message); } } else if (error.request) { // Request made but no response received throw new Error('Network error. Please check your connection.'); } else { // Something else happened throw new Error(error.message || 'Request failed'); } } };
Component Error Handling
// In component export default function ChatInterface() { const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const handleSubmit = async () => { setLoading(true); setError(null); try { const result = await generatePage(config, message, history); // Handle success } catch (err) { // Error is already transformed by API client setError(err.message); // Optional: Report to error tracking service console.error('Generation failed:', err); } finally { setLoading(false); } }; return ( <div> {error && ( <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded"> <p>{error}</p> <button onClick={() => setError(null)}>Dismiss</button> </div> )} {/* Rest of component */} </div> ); }
Request Configuration Patterns
With Timeout
export const generatePageWithTimeout = async (config, message, history, timeout = 60000) => { const response = await apiClient.post('/generate', { config, message, history }, { timeout // Override default timeout for long operations }); return response.data; };
With Abort Controller (Cancellation)
export const generatePageCancellable = async (config, message, history, signal) => { const response = await apiClient.post('/generate', { config, message, history }, { signal // Pass AbortController signal }); return response.data; }; // Usage in component const abortController = useRef(null); const handleGenerate = async () => { // Cancel previous request if exists if (abortController.current) { abortController.current.abort(); } abortController.current = new AbortController(); try { const result = await generatePageCancellable( config, message, history, abortController.current.signal ); // Handle result } catch (err) { if (err.name === 'AbortError') { console.log('Request cancelled'); } else { setError(err.message); } } }; // Cleanup on unmount useEffect(() => { return () => { if (abortController.current) { abortController.current.abort(); } }; }, []);
Streaming Responses Pattern
For long-running AI generation with streaming:
/** * Generate page with streaming response */ export const generatePageStream = async (config, message, history, onChunk) => { const response = await fetch(`${API_BASE}/generate/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ config, message, history }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); // Keep incomplete line in buffer for (const line of lines) { if (line.trim()) { try { const data = JSON.parse(line); onChunk(data); } catch (err) { console.error('Failed to parse chunk:', err); } } } } }; // Usage in component const handleStreamingGenerate = async () => { let fullText = ''; try { await generatePageStream(config, message, history, (chunk) => { // Process each chunk as it arrives if (chunk.type === 'text') { fullText += chunk.content; setPreview(fullText); // Update UI in real-time } else if (chunk.type === 'done') { setComplete(true); } }); } catch (err) { setError(err.message); } };
Retry Logic Pattern
For handling transient failures:
/** * Retry a function with exponential backoff */ const retryWithBackoff = async (fn, maxRetries = 3, baseDelay = 1000) => { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await fn(); } catch (error) { const isLastAttempt = attempt === maxRetries - 1; // Don't retry on 4xx errors (client errors) if (error.response?.status >= 400 && error.response?.status < 500) { throw error; } if (isLastAttempt) { throw error; } // Exponential backoff: 1s, 2s, 4s, 8s, etc. const delay = baseDelay * Math.pow(2, attempt); console.log(`Retry attempt ${attempt + 1}/${maxRetries} after ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } } }; // Usage export const generatePageWithRetry = async (config, message, history) => { return retryWithBackoff(async () => { const response = await apiClient.post('/generate', { config, message, history }); return response.data; }); };
File Upload Pattern
With Progress Tracking
export const uploadImageWithProgress = async (file, onProgress) => { const formData = new FormData(); formData.append('image', file); const response = await apiClient.post('/images/upload', formData, { headers: { 'Content-Type': 'multipart/form-data' }, onUploadProgress: (progressEvent) => { const percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); onProgress(percentCompleted); } }); return response.data; }; // Usage in component const handleFileUpload = async (file) => { setProgress(0); try { const result = await uploadImageWithProgress(file, (percent) => { setProgress(percent); }); setImageUrl(result.url); } catch (err) { setError(err.message); } finally { setProgress(0); } };
Environment-Based Configuration
// client/src/utils/api.js // Different base URLs for different environments const getApiBase = () => { const env = import.meta.env.MODE; switch (env) { case 'production': return 'https://api.yourapp.com'; case 'staging': return 'https://staging-api.yourapp.com'; case 'development': default: return import.meta.env.VITE_API_BASE || 'http://localhost:3001/api'; } }; const apiClient = axios.create({ baseURL: getApiBase(), timeout: 30000 });
Testing API Clients
// client/src/utils/api.test.js import { describe, it, expect, vi } from 'vitest'; import axios from 'axios'; import { generatePage, verifyPassword } from './api'; // Mock axios vi.mock('axios'); describe('API Client', () => { it('should call generatePage endpoint with correct data', async () => { const mockResponse = { data: { html: '<div>Test</div>', message: 'Generated successfully' } }; axios.create.mockReturnValue({ post: vi.fn().mockResolvedValue(mockResponse) }); const result = await generatePage( { depth: 2 }, 'Create a page about React', [] ); expect(result.html).toBe('<div>Test</div>'); }); it('should handle errors correctly', async () => { axios.create.mockReturnValue({ post: vi.fn().mockRejectedValue(new Error('Network error')) }); await expect( generatePage({}, 'Test', []) ).rejects.toThrow('Network error'); }); });
Checklist
Before Creating an API Client
- What endpoints will this client access?
- What base URL and configuration is needed?
- What error cases need handling?
- Does it need retry logic?
- Does it need request/response interceptors?
After Creating an API Client
- Base URL configured correctly
- Timeout set appropriately
- Errors transformed to consistent format
- All endpoints have JSDoc comments
- Request/response types documented
- Error cases handled in consuming components
- Tested with mock responses
Integration with Other Skills
- react-component-patterns: Using API clients in components
- express-api-patterns: Understanding backend endpoints
- systematic-debugging: Debugging API integration issues
- testing-patterns: Testing API clients
Common Mistakes to Avoid
- ❌ Scattered axios calls across components
- ❌ Hard-coded API URLs
- ❌ Not handling network errors
- ❌ Not transforming error responses
- ❌ Missing timeout configuration
- ❌ Not cancelling requests on unmount
- ❌ Retry logic on non-retryable errors (4xx)
- ❌ Not using environment variables for base URL