@smooai/fetch
Robust HTTP client built on the FetchBuilder pattern. Composable middleware for retries, circuit breaking, rate limiting, schema validation, and request/response hooks.
Overview
Stop writing the same retry logic over and over. @smooai/fetch handles the chaos of real-world APIs so you can focus on building features instead of handling failures. It works as a drop-in replacement for the native fetch API with smart defaults for retries, timeouts, and rate limit handling.
Smart Retries
Exponential backoff with jitter to prevent thundering herds. Only retries on network errors or 5xx responses.
Circuit Breaker
Stop hammering services that are down. Fail fast and recover gracefully after a cooldown period.
Schema Validation
Validate responses against Zod schemas for fully typed, runtime-safe API responses.
Features
FetchBuilder Pattern
Fluent, composable API for building configured fetch instances. Chain middleware together for your exact use case.
Rate Limit Handling
Automatically reads Retry-After headers and backs off intelligently on 429 responses.
Universal
Same API for Node.js and browsers. Zero external dependencies beyond the native Fetch API.
Request/Response Hooks
Pre-request and post-response lifecycle hooks for authentication, logging, and metrics collection.
Installation
pnpm add @smooai/fetchQuick Start
Build a resilient HTTP client with retries, circuit breaking, timeouts, and schema validation in a single fluent chain.
import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
const api = new FetchBuilder('https://api.example.com')
.withRetry({ maxRetries: 3, backoff: 'exponential' })
.withCircuitBreaker({ threshold: 5, resetTimeout: 30000 })
.withTimeout(5000)
.build();
const users = await api.get('/users', {
schema: z.array(z.object({ id: z.string(), name: z.string() })),
});FetchBuilder Pattern
The FetchBuilder provides a fluent interface for configuring fetch instances. Chain methods together to compose exactly the behavior you need, then call .build() to create a reusable client.
import { FetchBuilder, RetryMode } from '@smooai/fetch';
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const fetch = new FetchBuilder(UserSchema)
.withTimeout(5000)
.withRetry({
attempts: 3,
initialIntervalMs: 1000,
mode: RetryMode.JITTER,
})
.withRateLimit(100, 60000) // 100 requests per minute
.build();
const response = await fetch('https://api.example.com/users/123');
// response.data is typed as { id: string; name: string; email: string }Retry Strategies
Out of the box, @smooai/fetch retries failed requests with exponential backoff. Customize the retry behavior to match your needs.
import { FetchBuilder, RetryMode } from '@smooai/fetch';
const api = new FetchBuilder()
.withRetry({
attempts: 3, // Maximum retry attempts
initialIntervalMs: 1000, // Start with 1 second delay
mode: RetryMode.JITTER, // Add randomness to prevent thundering herds
factor: 2, // Double the delay each attempt
jitterAdjustment: 0.5, // Jitter range: 50% of current interval
onRejection: (error) => {
// Custom logic to decide whether to retry
if (error instanceof HTTPResponseError) {
return error.response.status >= 500;
}
return false;
},
})
.build();Default Retry Behavior
- 2 automatic retries on failure
- Exponential backoff: 500ms, 1s, 2s
- Jitter to prevent thundering herds
- Only retries on network errors or 5xx responses
Circuit Breaker
The circuit breaker pattern prevents your application from repeatedly calling a failing service. After a threshold of failures, it opens the circuit and fails fast, then periodically checks if the service has recovered.
const resilientApi = new FetchBuilder('https://api.example.com')
.withCircuitBreaker({
threshold: 5, // Open after 5 failures
resetTimeout: 30000, // Try again after 30 seconds
})
.withRetry({
maxRetries: 3,
backoff: 'exponential',
initialDelay: 1000,
})
.build();Closed
Normal operation. Requests pass through. Failures are counted.
Open
Threshold reached. Requests fail immediately without calling the service.
Half-Open
After reset timeout, one test request is allowed through to check recovery.
Schema Validation
Validate API responses at runtime using any Standard Schema-compatible validator (Zod, Valibot, ArkType). Get fully typed response data with automatic error handling.
import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const fetch = new FetchBuilder(UserSchema).build();
try {
const response = await fetch('https://api.example.com/users/123');
// response.data is typed as { id: string; name: string; email: string }
} catch (error) {
if (error instanceof HumanReadableSchemaError) {
console.error('Validation failed:', error.message);
}
}Request/Response Hooks
Add lifecycle hooks for cross-cutting concerns like authentication, logging, and metrics. Pre-request hooks can modify the URL and request configuration; post-response hooks handle success and error cases.
const api = new FetchBuilder()
.withHooks({
preRequest: (url, init) => {
// Add auth header to every request
init.headers = {
...init.headers,
Authorization: `Bearer ${getToken()}`,
};
return [url, init];
},
postResponseSuccess: (url, init, response) => {
// Track performance metrics
metrics.record({
endpoint: url.pathname,
duration: response.headers.get('x-response-time'),
status: response.status,
});
return response;
},
postResponseError: (url, init, error) => {
if (error.response?.status === 401) {
refreshToken(); // Token expired, refresh and retry
}
return error;
},
})
.build();Real-World Example: Graceful Degradation
Use circuit breakers with fallback services to keep your application running even when external APIs go down.
import { FetchBuilder } from '@smooai/fetch';
// Primary API with circuit breaker
const primaryAPI = new FetchBuilder()
.withCircuitBreaker({ failureThreshold: 3 })
.build();
// Fallback API with faster timeout
const fallbackAPI = new FetchBuilder()
.withTimeout(2000)
.build();
async function getWeather(city: string) {
try {
return await primaryAPI(`https://api1.weather.com/${city}`);
} catch {
// Seamlessly fall back to secondary service
console.warn('Primary weather API failed, using fallback');
return await fallbackAPI(`https://api2.weather.com/${city}`);
}
}