Accepted
The TypeScript client needs to provide the same resilience features as the Python client:
- Automatic retries with exponential backoff
- Rate limiting awareness (429 handling)
- Auto-pagination for GET requests
- Authentication header injection
The Python client achieves this through httpx's transport layer (a class-based approach). TypeScript doesn't have a direct equivalent, and we need a pattern that:
- Works with the native
fetchAPI - Is composable and testable
- Integrates with generated SDK code
- Works in both Node.js and browser environments
- Class-based transport wrapper: Similar to Python, create classes that wrap fetch
- Middleware pattern: Use a chain of middleware functions
- Composable fetch wrappers: Higher-order functions that wrap fetch
- Proxy-based interception: Use Proxy to intercept fetch calls
Use composable fetch wrappers - higher-order functions that take a fetch function and return a new fetch function with added behavior.
// Each wrapper adds one capability
const fetchWithRetry = createResilientFetch({ baseFetch: fetch });
const fetchWithPagination = createPaginatedFetch(fetchWithRetry);
const fetchWithAuth = createAuthenticatedFetch(fetchWithPagination, apiKey);The wrappers are composed in a specific order:
- Base fetch (globalThis.fetch or custom)
- Retry wrapper (handles retries with exponential backoff)
- Pagination wrapper (collects all pages for GET requests)
- Authentication wrapper (adds Authorization header)
User Request
│
▼
┌─────────────────────┐
│ Authentication │ ← Adds Bearer token
│ Wrapper │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Pagination │ ← Collects all pages (GET only)
│ Wrapper │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Retry │ ← Handles retries with backoff
│ Wrapper │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Base Fetch │ ← globalThis.fetch or custom
│ │
└─────────────────────┘
- Simple composition: Each wrapper has a single responsibility
- Easy testing: Each wrapper can be tested in isolation
- Custom fetch support: Users can provide their own base fetch
- Framework agnostic: Works with any framework that supports fetch
- Browser compatible: Uses standard fetch API
- Type safe: Full TypeScript support with generics
- Order matters: Wrappers must be composed in the correct order
- No shared state: Each request is independent (no connection pooling)
- Debugging complexity: Stack of wrappers can make debugging harder
- Different from Python: The pattern differs from Python's transport layer approach, but achieves the same goals
- No class hierarchy: Uses functions instead of classes, which is idiomatic for TypeScript/JavaScript
Each wrapper follows this pattern:
type FetchWrapper = (
baseFetch: typeof fetch,
options: WrapperOptions
) => typeof fetch;export function createResilientFetch(options: ResilientFetchOptions = {}): typeof fetch {
const config = { ...DEFAULT_RETRY_CONFIG, ...options.retry };
const baseFetch = options.baseFetch ?? globalThis.fetch;
return async (input, init) => {
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
try {
const response = await baseFetch(input, init);
if (response.ok || !shouldRetry(init?.method, response.status, config)) {
return response;
}
await sleep(calculateRetryDelay(attempt, config, response));
} catch (error) {
if (attempt === config.maxRetries) throw error;
await sleep(calculateRetryDelay(attempt, config));
}
}
throw new Error('Max retries exceeded');
};
}