Skip to content
/ fetch Public
generated from SmooAI/library-template

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.

License

Notifications You must be signed in to change notification settings

SmooAI/fetch

Repository files navigation


About SmooAI

SmooAI is an AI-powered platform for helping businesses multiply their customer, employee, and developer experience.

Learn more on smoo.ai

SmooAI Packages

Check out other SmooAI packages at npmjs.com/org/smooai

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.

NPM Version NPM Downloads NPM Last Update

GitHub License GitHub Actions Workflow Status GitHub Repo stars

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/fetch

The 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 normally

Production-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 production

Circuit 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

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

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

(back to examples)

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

(back to examples)

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

(back to examples)

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

(back to examples)

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

(back to examples)

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

(back to examples)

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

(back to examples)

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

(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

Contributing

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

Development Workflow

  1. Fork the repository

  2. Create your branch (git checkout -b amazing-feature)

  3. Make your changes

  4. 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
  5. Commit your changes (git commit -m 'Add some amazing feature')

  6. Push to the branch (git push origin feature/amazing-feature)

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

(back to top)

Contact

Brent Rager

Smoo Github: https://github.com/SmooAI

(back to top)

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •