Skip to content

SmooAI/fetch

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

107 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

@smooai/fetch β€” Resilient, type-safe HTTP for real-world APIs

npm Smoo AI license

TypeScript Python Rust Go

Features Β Β·Β  Install Β Β·Β  Usage Β Β·Β  Examples Β Β·Β  Platform


Stop writing the same retry logic over and over. @smooai/fetch is a drop-in fetch that 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.

✨ Features

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 β€” the same API for Node.js and browsers
  • πŸͺΆ Zero dependencies β€” just the fetch API and smart patterns

πŸ“¦ Install

pnpm add @smooai/fetch

Multi-language support

@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.

πŸš€ Usage

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 βœ…

Respect rate limits automatically

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 and browser

// 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 },
});

Schema validation that makes sense

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 production

Circuit breaking for critical services

import { 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.
}

πŸ“– Smart defaults

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.

Handle authentication globally

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();

Track performance automatically

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();

Graceful degradation

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}`);
    }
}

πŸ”— Pairs with @smooai/logger

@smooai/fetch works with @smooai/logger for complete observability across distributed systems.

Automatic correlation ID propagation

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.

Debug production issues faster

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"] }
// }

πŸ“š Examples

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,
        },
    },
});

(back to examples)

FetchBuilder pattern

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 }

(back to examples)

Retry

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();

(back to examples)

Timeout

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');
    }
}

(back to examples)

Rate limit

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();

(back to examples)

Schema validation

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
    }
}

(back to examples)

Lifecycle hooks

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
}

(back to examples)

Predefined authentication

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');

(back to examples)

Custom logger

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[]) => {
        /* ... */
    },
};

(back to examples)

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');
    }
}

(back to examples)

Built with

  • 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

🧩 Part of Smoo AI

@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.

🀝 Contributing

Contributions are welcome. This project uses changesets to manage versions and releases.

  1. Fork the repository.
  2. Create your branch (git checkout -b amazing-feature).
  3. Make your changes.
  4. Add a changeset to document them: pnpm changeset β€” it prompts for the version bump type (patch, minor, or major) and a description.
  5. Commit and push your branch.
  6. Open a pull request, referencing any related issues.

The maintainers will review your PR and may request changes before merging.

πŸ“„ License

MIT Β© SmooAI. See LICENSE.

Contact

Brent Rager

Smoo GitHub: github.com/SmooAI


Built by Smoo AI β€” AI built into every product.

About

Multi-language HTTP client (TypeScript, Python, Rust, Go) with smart retries, circuit breaking, rate limiting, request deduplication, and Standard Schema validation. Built on native fetch for Node.js and browser.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors