@smooai/fetch
v3.3.4
Published
A powerful fetch client library built on top of the native `fetch` API, designed for both Node.js and browser environments. Features built-in support for retries, timeouts, rate limiting, circuit breaking, and Standard Schema validation.
Readme
About Smoo AI
Smoo AI is an AI platform that helps businesses multiply their customer, employee, and developer experience — conversational AI for support and sales, paired with the production-grade developer tooling we use to build it.
This library is part of a small family of open-source packages we maintain to keep our own stack honest: contextual logging, typed HTTP, file storage, and agent orchestration. Use them in your stack, or take them as a reference for how we build.
- 🌐 smoo.ai — the product
- 📦 smoo.ai/open-source — every open-source package we ship
- 🐙 github.com/SmooAI — the source
About @smooai/fetch
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.
Multi-Language Support
@smooai/fetch is available as 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 code lives in the python/, rust/, and go/ directories.
Why @smooai/fetch?
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
Install
pnpm add @smooai/fetchThe Power of Resilient Fetching
Never Let a Hiccup Break Your App
Watch 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.
Respect Rate Limits Automatically
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 normallyProduction-Ready Examples
Node.js Usage
import fetch from '@smooai/fetch';
// It's just fetch, but resilient
const response = await fetch('https://api.example.com/users');
const users = await response.json();Browser Usage
import fetch from '@smooai/fetch/browser';
// Same API, different entry point
const response = await fetch('/api/checkout', {
method: 'POST',
body: { items: cart },
});Schema Validation That Makes Sense
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 productionCircuit Breaking for Critical Services
import { 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
}Real-World Scenarios
Handle Authentication Globally
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();Track Performance Automatically
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();Graceful Degradation
// 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}`);
}
}The Smart Defaults
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
Seamless Integration with @smooai/logger
@smooai/fetch works perfectly with @smooai/logger to provide complete observability across your distributed systems:
Automatic Correlation ID Propagation
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!Track Every Request with Context
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();Debug Production Issues Faster
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"]
// }
// }Examples
- 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
Basic Usage
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,
},
},
});FetchBuilder Pattern
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 }Retry Example
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();Timeout Example
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');
}
}Rate Limit Example
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();Schema Validation Example
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
}
}Lifecycle Hooks Example
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);
}Predefined Authentication Example
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');Custom Logger Example
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[]) => {
/* ... */
},
};Error Handling
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');
}
}Built With
- 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
Contributing
Contributions are welcome! This project uses changesets to manage versions and releases.
Development Workflow
Fork the repository
Create your branch (
git checkout -b amazing-feature)Make your changes
Add a changeset to document your changes:
pnpm changesetThis 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
Pull Request Guidelines
- Reference any related issues in your PR description
The maintainers will review your PR and may request changes before merging.
Contact
Brent Rager
Smoo Github: https://github.com/SmooAI
