Claude-skill-registry http-client-patterns
HTTP client patterns for axios configuration, interceptors, and request handling
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/http-client-patterns" ~/.claude/skills/majiayu000-claude-skill-registry-http-client-patterns && rm -rf "$T"
manifest:
skills/data/http-client-patterns/SKILL.mdsource content
HTTP Client Implementation Patterns
Core Principles
- ALL errors mapped to core types - never generic Error
- Proper connection cleanup - no resource leaks
- Appropriate timeouts - prevent hanging
- Retry logic for transient failures only
- No credential exposure in error messages
- Real connection validation - not just state checks
See error-handling skill for complete error mapping reference.
Client Class Structure
Basic Client Implementation
class GitHubClient { private httpClient: AxiosInstance; private token?: string; private baseUrl: string; async connect(profile: ConnectionProfile): Promise<void> { // Configure HTTP client this.baseUrl = profile.endpoint || 'https://api.github.com'; this.token = profile.credentials.token; // Set up HTTP client this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: 30000, headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } }); // Set up interceptors this.setupInterceptors(); // Validate connection with real API call await this.isConnected(); // Return void (not ConnectionState) } async isConnected(): Promise<boolean> { try { // Real API call to verify - NOT just checking stored state await this.httpClient.get('/user'); return true; } catch { return false; } } async disconnect(): Promise<void> { // Clean up resources this.httpClient = undefined; this.token = undefined; // Clear any cached data } }
Key points:
returnsconnect()
(not ConnectionState)Promise<void>
makes real API call (not state check)isConnected()
cleans up all resourcesdisconnect()- Use axios or similar HTTP client library
HTTP Client Configuration
Axios Client Setup
const client = axios.create({ baseURL: this.baseUrl, timeout: 30000, // 30 seconds headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' } });
Configuration options:
- API endpoint from connection profilebaseURL
- Reasonable timeout (30s typical)timeout
- Accept and content-type for JSON APIsheaders
Request Interceptor (Authentication)
// Request interceptor for auth client.interceptors.request.use(config => { if (this.token) { config.headers.Authorization = `Bearer ${this.token}`; } return config; });
Use cases:
- Add authentication headers
- Add request ID for tracing
- Log requests (without credentials!)
Response Interceptor (Error Handling)
// Response interceptor for errors client.interceptors.response.use( response => response, error => this.handleApiError(error) );
Error Handling Patterns
HTTP Status to Core Error Mapping
private handleApiError(error: any): never { const status = error.response?.status || 500; const message = error.response?.data?.message || error.message; switch (status) { case 401: throw new InvalidCredentialsError(); case 403: throw new UnauthorizedError(); case 404: throw new NoSuchObjectError('resource', 'id'); case 429: throw new RateLimitExceededError(); case 500: case 502: case 503: throw new ServiceUnavailableError(); default: throw new UnexpectedError(`API error: ${message}`, status); } }
Critical rules:
- ALWAYS map to core error types (see error-handling skill)
- NEVER throw generic
or leave unhandledError - Include status code in UnexpectedError
- Don't expose credentials in error messages
Safe Error Messages
// ❌ WRONG - Exposes credentials throw new Error(`Failed to connect with token ${this.token}`); // ✅ CORRECT - Safe error message throw new InvalidCredentialsError();
Producer Pattern
Basic Producer Structure
class UserProducer { constructor(private client: GitHubClient) {} async list(): Promise<User[]> { const response = await this.client.get('/users'); // Validate response format if (!Array.isArray(response.data)) { throw new UnexpectedError('Invalid response format'); } // Map to internal types return response.data.map(toUser); } async get(id: string): Promise<User> { const response = await this.client.get(`/users/${id}`); return toUser(response.data); } async create(data: CreateUserRequest): Promise<User> { const response = await this.client.post('/users', data); return toUser(response.data); } async update(id: string, data: UpdateUserRequest): Promise<User> { const response = await this.client.put(`/users/${id}`, data); return toUser(response.data); } async delete(id: string): Promise<void> { await this.client.delete(`/users/${id}`); } }
Key patterns:
- Producer depends on client (injected)
- Each operation is async method
- Use mapper functions to convert responses
- Validate response format before mapping
- Return appropriate types (arrays, objects, void)
Connection Validation
Pattern: Real API Call
async isConnected(): Promise<boolean> { try { // Real API call - lightweight endpoint await this.client.get('/user'); return true; } catch { return false; } }
Why real API call:
- Verifies network connectivity
- Confirms credentials still valid
- Detects API availability
- Not just checking stored state
Anti-pattern:
// ❌ WRONG - Just checking state async isConnected(): Promise<boolean> { return this.token !== undefined; }
Retry Logic
Pattern: Exponential Backoff
// For transient failures only (5xx errors, timeouts) async function withRetry<T>( operation: () => Promise<T>, maxAttempts = 3 ): Promise<T> { for (let i = 0; i < maxAttempts; i++) { try { return await operation(); } catch (error) { // Don't retry client errors (4xx) if (error.response?.status >= 400 && error.response?.status < 500) { throw error; } // Last attempt - throw error if (i === maxAttempts - 1) { throw error; } // Exponential backoff: 1s, 2s, 4s await delay(Math.pow(2, i) * 1000); } } } function delay(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }
Retry rules:
- Only retry transient failures (5xx, network errors)
- NEVER retry client errors (4xx) - they won't succeed
- Use exponential backoff (1s, 2s, 4s)
- Limit retries (3 attempts typical)
Using Retry in Client
async get(path: string): Promise<any> { return withRetry(() => this.httpClient.get(path)); }
Response Validation
Validate Before Mapping
async list(): Promise<User[]> { const response = await this.client.get('/users'); // ✅ CORRECT - Validate response format if (!Array.isArray(response.data)) { throw new UnexpectedError('Invalid response format: expected array'); } return response.data.map(toUser); }
Validation checks:
- Array responses:
Array.isArray(data) - Object responses:
typeof data === 'object' && data !== null - Required fields:
if (!data.id) throw ...
Timeout Configuration
Setting Timeouts
const client = axios.create({ baseURL: this.baseUrl, timeout: 30000, // 30 seconds (typical) });
Timeout guidelines:
- Fast operations (list, get): 10-30 seconds
- Slow operations (search, reports): 60-120 seconds
- Long-running (exports, batch): 300+ seconds
- Default: 30 seconds
Per-Request Timeout
async longRunningOperation(): Promise<Result> { return this.client.get('/export', { timeout: 120000 // 2 minutes for this specific call }); }
Connection Cleanup
Proper Disconnect Pattern
async disconnect(): Promise<void> { // Clear client reference this.httpClient = undefined; // Clear credentials this.token = undefined; // Clear any cached data this.cache?.clear(); // Cancel pending requests (if using axios) this.cancelTokenSource?.cancel('Connection closed'); }
Cleanup checklist:
- Clear HTTP client reference
- Clear authentication tokens
- Clear cached data
- Cancel pending requests
- Remove event listeners (if any)
Quality Standards
Zero Tolerance For:
- Unhandled promise rejections
- Memory leaks in connections
- Synchronous blocking operations
- Missing error handling
- Generic Error thrown
- Credentials in error messages
Must Ensure:
- All errors mapped to core types
- Proper connection cleanup
- Appropriate timeouts set
- Retry logic for transient failures only
- Real connection validation
- Response validation before mapping
Common Anti-Patterns
❌ WRONG: Generic Errors
throw new Error('API call failed'); // Generic!
✅ CORRECT: Core Error Types
throw new ServiceUnavailableError(); // Core type
❌ WRONG: No Response Validation
return response.data.map(toUser); // What if not array?
✅ CORRECT: Validate First
if (!Array.isArray(response.data)) { throw new UnexpectedError('Invalid response format'); } return response.data.map(toUser);
❌ WRONG: State-Based Connection Check
async isConnected(): Promise<boolean> { return this.token !== undefined; // Just checking state! }
✅ CORRECT: Real API Call
async isConnected(): Promise<boolean> { try { await this.client.get('/user'); return true; } catch { return false; } }
Testing Patterns
Mock HTTP Responses (with nock)
import nock from 'nock'; describe('UserProducer', () => { it('should list users', async () => { nock('https://api.github.com') .get('/users') .reply(200, [ { id: '1', name: 'Alice' }, { id: '2', name: 'Bob' } ]); const users = await producer.list(); expect(users).toHaveLength(2); }); });
See nock-patterns skill for complete mocking patterns.
Success Metrics
HTTP client implementation MUST meet all criteria:
- ✅ All API calls succeed in integration tests
- ✅ Proper error handling coverage (all status codes)
- ✅ No connection leaks (cleanup verified)
- ✅ Optimal performance (timeouts configured)
- ✅ Clean retry patterns (transient failures only)
- ✅ All errors map to core types (ZERO generic errors)
- ✅ Response validation before mapping
- ✅ Real connection validation (not state-based)