Open Source Package

@smooai/fetch

Robust HTTP client built on the FetchBuilder pattern. Composable middleware for retries, circuit breaking, rate limiting, schema validation, and request/response hooks.

Overview

Stop writing the same retry logic over and over. @smooai/fetch handles the chaos of real-world APIs so you can focus on building features instead of handling failures. It works as a drop-in replacement for the native fetch API with smart defaults for retries, timeouts, and rate limit handling.

Smart Retries

Exponential backoff with jitter to prevent thundering herds. Only retries on network errors or 5xx responses.

Circuit Breaker

Stop hammering services that are down. Fail fast and recover gracefully after a cooldown period.

Schema Validation

Validate responses against Zod schemas for fully typed, runtime-safe API responses.

Features

FetchBuilder Pattern

Fluent, composable API for building configured fetch instances. Chain middleware together for your exact use case.

Rate Limit Handling

Automatically reads Retry-After headers and backs off intelligently on 429 responses.

Universal

Same API for Node.js and browsers. Zero external dependencies beyond the native Fetch API.

Request/Response Hooks

Pre-request and post-response lifecycle hooks for authentication, logging, and metrics collection.

Installation

pnpm add @smooai/fetch

Quick Start

Build a resilient HTTP client with retries, circuit breaking, timeouts, and schema validation in a single fluent chain.

import { FetchBuilder } from '@smooai/fetch';
import { z } from 'zod';

const api = new FetchBuilder('https://api.example.com')
    .withRetry({ maxRetries: 3, backoff: 'exponential' })
    .withCircuitBreaker({ threshold: 5, resetTimeout: 30000 })
    .withTimeout(5000)
    .build();

const users = await api.get('/users', {
    schema: z.array(z.object({ id: z.string(), name: z.string() })),
});

FetchBuilder Pattern

The FetchBuilder provides a fluent interface for configuring fetch instances. Chain methods together to compose exactly the behavior you need, then call .build() to create a reusable client.

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

Retry Strategies

Out of the box, @smooai/fetch retries failed requests with exponential backoff. Customize the retry behavior to match your needs.

import { FetchBuilder, RetryMode } from '@smooai/fetch';

const api = new FetchBuilder()
    .withRetry({
        attempts: 3,               // Maximum retry attempts
        initialIntervalMs: 1000,   // Start with 1 second delay
        mode: RetryMode.JITTER,    // Add randomness to prevent thundering herds
        factor: 2,                 // Double the delay each attempt
        jitterAdjustment: 0.5,     // Jitter range: 50% of current interval
        onRejection: (error) => {
            // Custom logic to decide whether to retry
            if (error instanceof HTTPResponseError) {
                return error.response.status >= 500;
            }
            return false;
        },
    })
    .build();

Default Retry Behavior

  • 2 automatic retries on failure
  • Exponential backoff: 500ms, 1s, 2s
  • Jitter to prevent thundering herds
  • Only retries on network errors or 5xx responses

Circuit Breaker

The circuit breaker pattern prevents your application from repeatedly calling a failing service. After a threshold of failures, it opens the circuit and fails fast, then periodically checks if the service has recovered.

const resilientApi = new FetchBuilder('https://api.example.com')
    .withCircuitBreaker({
        threshold: 5,        // Open after 5 failures
        resetTimeout: 30000, // Try again after 30 seconds
    })
    .withRetry({
        maxRetries: 3,
        backoff: 'exponential',
        initialDelay: 1000,
    })
    .build();

Closed

Normal operation. Requests pass through. Failures are counted.

Open

Threshold reached. Requests fail immediately without calling the service.

Half-Open

After reset timeout, one test request is allowed through to check recovery.

Schema Validation

Validate API responses at runtime using any Standard Schema-compatible validator (Zod, Valibot, ArkType). Get fully typed response data with automatic error handling.

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

Request/Response Hooks

Add lifecycle hooks for cross-cutting concerns like authentication, logging, and metrics. Pre-request hooks can modify the URL and request configuration; post-response hooks handle success and error cases.

const api = new FetchBuilder()
    .withHooks({
        preRequest: (url, init) => {
            // Add auth header to every request
            init.headers = {
                ...init.headers,
                Authorization: `Bearer ${getToken()}`,
            };
            return [url, init];
        },
        postResponseSuccess: (url, init, response) => {
            // Track performance metrics
            metrics.record({
                endpoint: url.pathname,
                duration: response.headers.get('x-response-time'),
                status: response.status,
            });
            return response;
        },
        postResponseError: (url, init, error) => {
            if (error.response?.status === 401) {
                refreshToken(); // Token expired, refresh and retry
            }
            return error;
        },
    })
    .build();

Real-World Example: Graceful Degradation

Use circuit breakers with fallback services to keep your application running even when external APIs go down.

import { FetchBuilder } from '@smooai/fetch';

// Primary API with circuit breaker
const primaryAPI = new FetchBuilder()
    .withCircuitBreaker({ failureThreshold: 3 })
    .build();

// Fallback API with faster timeout
const fallbackAPI = new FetchBuilder()
    .withTimeout(2000)
    .build();

async function getWeather(city: string) {
    try {
        return await primaryAPI(`https://api1.weather.com/${city}`);
    } catch {
        // Seamlessly fall back to secondary service
        console.warn('Primary weather API failed, using fallback');
        return await fallbackAPI(`https://api2.weather.com/${city}`);
    }
}