Features Β Β·Β Install Β Β·Β Usage Β Β·Β Examples Β Β·Β Platform
Stop writing the same retry logic over and over.
@smooai/fetchis a drop-infetchthat survives the reality of network failures β exponential backoff, timeouts, rate-limit awareness, circuit breaking, and schema-validated responses β so you can focus on features instead of failure handling. Same API in Node.js and the browser, with native ports in TypeScript, Python, Rust, and Go.
Traditional fetch gives you the request, but leaves you to handle the reality of flaky APIs, slow endpoints, and rate limits. @smooai/fetch handles them by default.
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-Afterheaders 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 β the same API for Node.js and browsers
- πͺΆ Zero dependencies β just the fetch API and smart patterns
pnpm add @smooai/fetch@smooai/fetch ships native implementations in TypeScript, Python, Rust, and Go β each built with idiomatic patterns for its ecosystem.
| Language | Package | Install |
|---|---|---|
| TypeScript | @smooai/fetch |
pnpm add @smooai/fetch |
| Python | smooai-fetch |
pip install smooai-fetch |
| Rust | smooai-fetch |
cargo add smooai-fetch |
| Go | github.com/SmooAI/fetch/go/fetch |
go get github.com/SmooAI/fetch/go/fetch |
Language-specific source lives in the python/, rust/, and go/ directories.
It's just fetch, but resilient.
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 β
const response = await fetch('https://api.github.com/user/repos');
// If GitHub says "slow down":
// - Sees 429 + Retry-After: 60
// - Automatically waits 60 seconds
// - Retries and succeeds// Node.js
import fetch from '@smooai/fetch';
const response = await fetch('https://api.example.com/users');
const users = await response.json();
// Browser β same API, different entry point
import fetch from '@smooai/fetch/browser';
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(),
});
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';
const criticalAPI = new FetchBuilder()
.withCircuitBreaker({
failureThreshold: 5, // 5 failures
failureWindow: 60000, // in 60 seconds
recoveryTime: 30000, // try again after 30s
})
.build();
try {
await criticalAPI('https://payment-processor.com/charge');
} catch (error) {
// Circuit is open β service is down. Show fallback UI immediately.
}Out of the box, @smooai/fetch is configured for the real world:
Retry strategy β 2 automatic retries, exponential backoff (500ms β 1s β 2s), jitter to prevent thundering herds, and retries only on network errors or 5xx responses.
Timeout protection β 10-second default timeout, configurable per request, so requests never hang indefinitely.
Rate-limit handling β respects Retry-After headers and backs off automatically on 429 responses.
const api = new FetchBuilder()
.withHooks({
preRequest: (url, init) => {
init.headers = {
...init.headers,
Authorization: `Bearer ${getToken()}`,
};
return [url, init];
},
postResponseError: (url, init, error) => {
if (error.response?.status === 401) {
refreshToken(); // Token expired β refresh and retry
}
return error;
},
})
.build();const api = new FetchBuilder()
.withHooks({
postResponseSuccess: (url, init, response) => {
metrics.record({
endpoint: url.pathname,
duration: response.headers.get('x-response-time'),
status: response.status,
});
return response;
},
})
.build();const primaryAPI = new FetchBuilder().withCircuitBreaker({ failureThreshold: 3 }).build();
const fallbackAPI = new FetchBuilder().withTimeout(2000).build();
async function getWeather(city: string) {
try {
return await primaryAPI(`https://api1.weather.com/${city}`);
} catch (error) {
console.warn('Primary weather API failed, using fallback');
return await fallbackAPI(`https://api2.weather.com/${city}`);
}
}@smooai/fetch works with @smooai/logger for complete observability across distributed systems.
import fetch, { FetchBuilder } from '@smooai/fetch';
import { AwsServerLogger } from '@smooai/logger/AwsServerLogger';
const logger = new AwsServerLogger({ name: 'APIClient' });
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, the correlation ID is automatically extracted and logs are linked.When something goes wrong, you have the complete story β initial request, each retry attempt, circuit-breaker state changes, and the final error with a full stack trace:
try {
const response = await api('/flaky-endpoint');
} catch (error) {
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
- Timeout
- Rate limit
- Schema validation
- Lifecycle hooks
- Predefined authentication
- Custom logger
- 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';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
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();
const response = await fetch('https://api.example.com/users/123');
// response.data is 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) => {
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';
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';
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
const fetch = new FetchBuilder(UserSchema)
.withHooks({
// Pre-request hook can modify both URL and request configuration
preRequest: (url, init) => {
const modifiedUrl = new URL(url.toString());
modifiedUrl.searchParams.set('timestamp', Date.now().toString());
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) {
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) {
return new Error(`Request to ${url} failed with status ${error.response.status}. ` + `Method: ${init.method}`);
}
return error;
},
})
.build();
try {
const response = await fetch('https://api.example.com/users/123');
console.log(response.data); // includes the _metadata added by postResponseSuccess
} catch (error) {
console.error(error.message); // includes details added by postResponseError
}import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';
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 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
});
const fetch = new FetchBuilder(
z.object({
id: z.string(),
name: z.string(),
}),
)
.withLogger(logger)
.build();
// All requests now include correlation IDs, performance tracking,
// full error context, and 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 and rate limiter
- Standard Schema
- @smooai/logger β structured logging (bring your own logger supported)
- @smooai/utils β Standard Schema validation and human-readable error generation
@smooai/fetch is built and open-sourced by Smoo AI β the AI-powered business platform with AI built into every product: CRM, customer support, campaigns, field service, observability, and developer tools.
- π§° More open source from Smoo AI β smoo.ai/open-source
- π§© Sibling packages β @smooai/logger, @smooai/config, smooth
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 them:
pnpm changesetβ it prompts for the version bump type (patch, minor, or major) and a description. - Commit and push your branch.
- Open a pull request, referencing any related issues.
The maintainers will review your PR and may request changes before merging.
MIT Β© SmooAI. See LICENSE.
Brent Rager
Smoo GitHub: github.com/SmooAI
Built by Smoo AI β AI built into every product.
