SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.
Learn more on smoo.ai
Check out other SmooAI packages at npmjs.com/org/smooai
Stop writing the same retry logic over and over - A resilient HTTP client that handles the chaos of real-world APIs, so you can focus on building features instead of handling failures.
Ever had your app crash because an API was down for 2 seconds? Or watched your users stare at loading spinners because a third-party service hit its rate limit? Traditional fetch gives you the request, but leaves you to handle the reality of network failures.
@smooai/fetch automatically handles:
For Unreliable APIs:
- 🔄 Smart retries - Exponential backoff with jitter to prevent thundering herds
- ⏱️ Automatic timeouts - Never hang indefinitely on slow endpoints
- 🚦 Rate limit respect - Reads Retry-After headers and backs off intelligently
- 🔌 Circuit breaking - Stop hammering services that are clearly down
- ⚡ Request deduplication - Prevent duplicate in-flight requests
For Developer Experience:
- 🎯 Type-safe responses - Schema validation with any Standard Schema validator
- 🔗 Request lifecycle - Pre/post hooks for authentication and logging
- 📊 Built-in telemetry - Track success rates and response times
- 🌐 Universal - Same API for Node.js and browsers
- 🪶 Zero dependencies - Just the fetch API and smart patterns
pnpm add @smooai/fetchWatch how @smooai/fetch handles common failure scenarios:
import fetch from '@smooai/fetch';
// This won't crash if the API is temporarily down
const response = await fetch('https://flaky-api.com/data');
// Behind the scenes:
// Attempt 1: 500 error - waits 500ms
// Attempt 2: 503 error - waits 1000ms
// Attempt 3: 200 success! ✅Your users never know the API had issues - the request just works.
No more manual retry-after parsing:
const response = await fetch('https://api.github.com/user/repos');
// If GitHub says "slow down":
// - Sees 429 status + Retry-After: 60
// - Automatically waits 60 seconds
// - Retries and succeeds
// - Your code continues normallyimport fetch from '@smooai/fetch';
// It's just fetch, but resilient
const response = await fetch('https://api.example.com/users');
const users = await response.json();import fetch from '@smooai/fetch/browser';
// Same API, different entry point
const response = await fetch('/api/checkout', {
method: 'POST',
body: { items: cart },
});import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
});
// Your API returns garbage? You'll know immediately
const response = await fetch('https://api.example.com/user', {
options: { schema: UserSchema },
});
// response.data is fully typed as { id: string; email: string }
// No more runtime surprises in productionimport { FetchBuilder } from '@smooai/fetch';
// Stop hammering services that are clearly struggling
const criticalAPI = new FetchBuilder()
.withCircuitBreaker({
failureThreshold: 5, // 5 failures
failureWindow: 60000, // in 60 seconds
recoveryTime: 30000, // try again after 30s
})
.build();
// If the service is down, this fails fast instead of waiting
try {
await criticalAPI('https://payment-processor.com/charge');
} catch (error) {
// Circuit is open - service is down
// Show fallback UI immediately
}const api = new FetchBuilder()
.withHooks({
preRequest: (url, init) => {
// Add auth header to every request
init.headers = {
...init.headers,
Authorization: `Bearer ${getToken()}`,
};
return [url, init];
},
postResponseError: (url, init, error) => {
if (error.response?.status === 401) {
// Token expired - refresh and retry
refreshToken();
}
return error;
},
})
.build();const api = new FetchBuilder()
.withHooks({
postResponseSuccess: (url, init, response) => {
// Send metrics to your monitoring service
metrics.record({
endpoint: url.pathname,
duration: response.headers.get('x-response-time'),
status: response.status,
});
return response;
},
})
.build();// Primary API with circuit breaker
const primaryAPI = new FetchBuilder().withCircuitBreaker({ failureThreshold: 3 }).build();
// Fallback API for resilience
const fallbackAPI = new FetchBuilder()
.withTimeout(2000) // Faster timeout for fallback
.build();
async function getWeather(city: string) {
try {
return await primaryAPI(`https://api1.weather.com/${city}`);
} catch (error) {
// Seamlessly fall back to secondary service
console.warn('Primary weather API failed, using fallback');
return await fallbackAPI(`https://api2.weather.com/${city}`);
}
}Out of the box, @smooai/fetch is configured for the real world:
Retry Strategy:
- 2 automatic retries on failure
- Exponential backoff: 500ms → 1s → 2s
- Jitter to prevent thundering herds
- Only retries on network errors or 5xx responses
Timeout Protection:
- 10-second default timeout
- Prevents indefinite hangs
- Configurable per request
Rate Limit Handling:
- Respects Retry-After headers
- Automatic backoff on 429 responses
- Prevents API ban hammers
@smooai/fetch works perfectly with @smooai/logger to provide complete observability across your distributed systems:
import fetch from '@smooai/fetch';
import { AwsServerLogger } from '@smooai/logger/AwsServerLogger';
const logger = new AwsServerLogger({ name: 'APIClient' });
// Correlation IDs flow automatically through your requests
const api = new FetchBuilder()
.withLogger(logger) // That's it!
.build();
// In Service A
logger.info('Starting user flow'); // Correlation ID: abc-123
const user = await api('/users/123'); // Correlation ID sent as header
// In Service B (receiving the request)
// The correlation ID is automatically extracted and logs are linked!const api = new FetchBuilder()
.withLogger(logger)
.withHooks({
postResponseSuccess: (url, init, response) => {
// Logger automatically captures:
// - Correlation ID
// - Request method & URL
// - Response status
// - Duration
// - Any errors with full context
logger.info('API request completed', {
endpoint: url.pathname,
status: response.status,
});
return response;
},
})
.build();When something goes wrong, you'll have the complete story:
try {
const response = await api('/flaky-endpoint');
} catch (error) {
// Logger captures the entire request lifecycle:
// - Initial request with headers
// - Each retry attempt
// - Circuit breaker state changes
// - Final error with full stack trace
logger.error('Request failed after retries', error);
}
// In your logs:
// {
// "correlationId": "abc-123",
// "message": "Request failed after retries",
// "error": {
// "attempts": 3,
// "lastError": "TimeoutError",
// "circuitState": "open"
// },
// "callerContext": {
// "stack": ["/src/services/UserService.ts:42:16"]
// }
// }- Basic Usage
- FetchBuilder Pattern
- Retry Example
- Timeout Example
- Rate Limit Example
- Circuit Breaker Example
- Schema Validation Example
- Predefined Authentication Example
- Custom Logger Example
- Error Handling
import fetch from '@smooai/fetch';
// Simple GET request
const response = await fetch('https://api.example.com/data');
// POST request with JSON body and options
const response = await fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: {
key: 'value',
},
options: {
timeout: {
timeoutMs: 5000,
},
retry: {
attempts: 3,
},
},
});The FetchBuilder provides a fluent interface for configuring fetch instances:
import { FetchBuilder, RetryMode } from '@smooai/fetch';
import { z } from 'zod';
// Define a response schema
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Create a configured fetch instance
const fetch = new FetchBuilder(UserSchema)
.withTimeout(5000) // 5 second timeout
.withRetry({
attempts: 3,
initialIntervalMs: 1000,
mode: RetryMode.JITTER,
})
.withRateLimit(100, 60000) // 100 requests per minute
.build();
// Use the configured fetch instance
const response = await fetch('https://api.example.com/users/123');
// response.data is now typed as { id: string; name: string; email: string }import { FetchBuilder, RetryMode } from '@smooai/fetch';
// Using the default fetch
const response = await fetch('https://api.example.com/data', {
options: {
retry: {
attempts: 3,
initialIntervalMs: 1000,
mode: RetryMode.JITTER,
factor: 2,
jitterAdjustment: 0.5,
onRejection: (error) => {
// Custom retry logic
if (error instanceof HTTPResponseError) {
return error.response.status >= 500;
}
return false;
},
},
},
});
// Or using FetchBuilder
const fetch = new FetchBuilder()
.withRetry({
attempts: 3,
initialIntervalMs: 1000,
mode: RetryMode.JITTER,
factor: 2,
jitterAdjustment: 0.5,
onRejection: (error) => {
if (error instanceof HTTPResponseError) {
return error.response.status >= 500;
}
return false;
},
})
.build();import { FetchBuilder } from '@smooai/fetch';
// Using the default fetch
const response = await fetch('https://api.example.com/slow-endpoint', {
options: {
timeout: {
timeoutMs: 5000,
},
},
});
// Or using FetchBuilder
const fetch = new FetchBuilder()
.withTimeout(5000) // 5 second timeout
.build();
try {
const response = await fetch('https://api.example.com/slow-endpoint');
} catch (error) {
if (error instanceof TimeoutError) {
console.error('Request timed out');
}
}import { FetchBuilder } from '@smooai/fetch';
// Using the default fetch
const response = await fetch('https://api.example.com/data', {
options: {
retry: {
attempts: 1,
initialIntervalMs: 1000,
onRejection: (error) => {
if (error instanceof RatelimitError) {
return error.remainingTimeInRatelimit;
}
return false;
},
},
},
});
// Or using FetchBuilder
const fetch = new FetchBuilder()
.withRateLimit(100, 60000, {
attempts: 1,
initialIntervalMs: 1000,
onRejection: (error) => {
if (error instanceof RatelimitError) {
return error.remainingTimeInRatelimit;
}
return false;
},
})
.build();import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
// Define response schema
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Using the default fetch
const response = await fetch('https://api.example.com/users/123', {
options: {
schema: UserSchema,
},
});
// Or using FetchBuilder
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);
// Example output:
// Validation failed: Invalid email format at path: email
}
}import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
// Define response schema
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Create a fetch instance with hooks
const fetch = new FetchBuilder(UserSchema)
.withHooks({
// Pre-request hook can modify both URL and request configuration
preRequest: (url, init) => {
// Add timestamp to URL
const modifiedUrl = new URL(url.toString());
modifiedUrl.searchParams.set('timestamp', Date.now().toString());
// Add custom headers
init.headers = {
...init.headers,
'X-Custom-Header': 'value',
};
return [modifiedUrl, init];
},
// Post-response success hook can modify the response
// Note: url and init are readonly in this hook
postResponseSuccess: (url, init, response) => {
if (response.isJson && response.data) {
// Add request metadata to response
response.data = {
...response.data,
_metadata: {
requestUrl: url.toString(),
requestMethod: init.method,
processedAt: new Date().toISOString(),
},
};
}
return response;
},
// Post-response error hook can handle or transform errors
// Note: url and init are readonly in this hook
postResponseError: (url, init, error, response) => {
if (error instanceof HTTPResponseError) {
// Create a more detailed error message
return new Error(`Request to ${url} failed with status ${error.response.status}. ` + `Method: ${init.method}`);
}
return error;
},
})
.build();
// Use the configured fetch instance
try {
const response = await fetch('https://api.example.com/users/123');
// response.data includes the _metadata added by postResponseSuccess
console.log(response.data);
} catch (error) {
// Error message includes details added by postResponseError
console.error(error.message);
}import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
// Define response schema
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Using the default fetch
const response = await fetch('https://api.example.com/users/123', {
headers: {
Authorization: 'Bearer your-auth-token',
'X-API-Key': 'your-api-key',
'X-Client-ID': 'your-client-id',
},
options: {
schema: UserSchema,
},
});
// Or using FetchBuilder
const fetch = new FetchBuilder(UserSchema)
.withInit({
headers: {
Authorization: 'Bearer your-auth-token',
'X-API-Key': 'your-api-key',
'X-Client-ID': 'your-client-id',
},
})
.build();
// All requests will automatically include the auth headers
const response = await fetch('https://api.example.com/users/123');import { FetchBuilder } from '@smooai/fetch';
import { AwsServerLogger } from '@smooai/logger/AwsServerLogger';
import { z } from 'zod';
// Use @smooai/logger for automatic context and correlation
const logger = new AwsServerLogger({
name: 'MyAPI',
prettyPrint: true, // Human-readable logs in development
});
// Create a fetch instance with the logger
const fetch = new FetchBuilder(
z.object({
id: z.string(),
name: z.string(),
}),
)
.withLogger(logger)
.build();
// All requests now include:
// - Correlation IDs that flow across services
// - Automatic performance tracking
// - Full error context with stack traces
// - Request/response details
const response = await fetch('https://api.example.com/users/123');
// Or bring your own logger that implements LoggerInterface
const customLogger = {
debug: (message: string, ...args: any[]) => {
/* ... */
},
info: (message: string, ...args: any[]) => {
/* ... */
},
warn: (message: string, ...args: any[]) => {
/* ... */
},
error: (error: Error | unknown, message: string, ...args: any[]) => {
/* ... */
},
};import fetch, { HTTPResponseError, RatelimitError, RetryError, TimeoutError } from '@smooai/fetch';
try {
const response = await fetch('https://api.example.com/data');
} catch (error) {
if (error instanceof HTTPResponseError) {
console.error('HTTP Error:', error.response.status);
console.error('Response Data:', error.response.data);
} else if (error instanceof RetryError) {
console.error('Retry failed after all attempts');
} else if (error instanceof TimeoutError) {
console.error('Request timed out');
} else if (error instanceof RatelimitError) {
console.error('Rate limit exceeded');
}
}- TypeScript
- Native Fetch API
- Mollitia (Circuit Breaker, Rate Limiter)
- Standard Schema
- @smooai/logger for structured logging (bring your own logger supported)
- @smooai/utils for Standard Schema validation and human-readable error generation
Contributions are welcome! This project uses changesets to manage versions and releases.
-
Fork the repository
-
Create your branch (
git checkout -b amazing-feature) -
Make your changes
-
Add a changeset to document your changes:
pnpm changeset
This will prompt you to:
- Choose the type of version bump (patch, minor, or major)
- Provide a description of the changes
-
Commit your changes (
git commit -m 'Add some amazing feature') -
Push to the branch (
git push origin feature/amazing-feature) -
Open a Pull Request
- Reference any related issues in your PR description
The maintainers will review your PR and may request changes before merging.
Brent Rager
Smoo Github: https://github.com/SmooAI