1.6.0 • Published 23 days ago

@smooai/fetch v1.6.0

Weekly downloads
-
License
MIT
Repository
github
Last release
23 days ago

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

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.

NPM Version NPM Downloads NPM Last Update

GitHub License GitHub Actions Workflow Status GitHub Repo stars

Install

pnpm add @smooai/fetch

Usage

The package provides two entry points:

  • @smooai/fetch - For Node.js environments
  • @smooai/fetch/browser - For browser environments

Node.js Usage

import fetch from '@smooai/fetch';

// Simple GET request
const response = await fetch('https://api.example.com/data');

Browser Usage

import fetch from '@smooai/fetch/browser';

// Simple GET request
const response = await fetch('https://api.example.com/data');

Key Features

🚀 Native Fetch API

  • Built on top of the native fetch API
  • Works seamlessly in both Node.js and browser environments
  • Full TypeScript support
  • Automatic JSON parsing and stringifying
  • Structured error handling with detailed response information

⚙️ Opinionated Defaults

The default export fetch comes with carefully chosen defaults for common use cases:

  • Retry Configuration

    • 2 retry attempts
    • 500ms initial interval with jitter
    • Exponential backoff with factor of 2
    • 0.5 jitter adjustment
    • Smart retry decisions based on:
      • HTTP 5xx errors
      • Rate limit responses (429)
      • Timeout errors
      • Retry-After header support
  • Timeout Settings

    • 10 second timeout for all requests
    • Automatic timeout error handling
  • Rate Limit Retry

    • 1 retry attempt for rate limit errors
    • 500ms initial interval
    • Smart handling of rate limit headers
    • 50ms buffer added to retry timing

These defaults are designed to handle common API integration scenarios while providing a good balance between reliability and performance. They can be overridden using the FetchBuilder pattern or by passing custom options to the default fetch function.

✅ Schema Validation

  • Built-in support for Standard Schema compatible validators
  • Works with Zod, ArkType, and other Standard Schema implementations
  • Type-safe response validation
  • Human-readable validation errors
  • Typed responses

🔄 Lifecycle Hooks

  • Pre-request Hook

    • Modify URL and request configuration before sending
    • Add custom headers, query parameters, or transform request body
    • Full access to modify both URL and request init
  • Post-response Success Hook

    • Transform successful responses after schema validation
    • Add metadata or transform response data
    • Read-only access to original request details
  • Post-response Error Hook

    • Handle or transform errors before they're thrown
    • Create detailed error messages with request context
    • Read-only access to original request details
  • Type Safety

    • Fully typed with TypeScript
    • Non-editable parameters marked as readonly
    • Schema types preserved throughout lifecycle
  • Integration

    • Works seamlessly with schema validation
    • Compatible with retry, rate limiting, and circuit breaking
    • Preserves request/response context

🛡️ Resilience Features

  • Retry Mechanism

    • Configurable retry attempts and intervals
    • Jitter support for distributed retries
    • Smart retry decisions based on response status
    • Automatic handling of Retry-After headers
    • Custom retry callbacks
  • Timeout Control

    • Configurable timeout duration
    • Optional retry on timeout
    • Automatic timeout error handling
  • Rate Limiting

    • Configurable request limits per time period
    • Automatic rate limit header handling
    • Smart retry on rate limit errors
    • Custom rate limit retry strategies
  • Circuit Breaking

    • Sliding window failure rate tracking
    • Configurable failure thresholds
    • Half-open state support
    • Automatic recovery
    • Custom error callbacks

🔄 Automatic Context

  • Automatic context propagation (correlation IDs, user agents)
  • Structured logging integration

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

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 { z } from 'zod';

// Create a custom logger that implements the LoggerInterface
const customLogger = {
    debug: (message: string, ...args: any[]) => {
        console.debug(`[DEBUG] ${message}`, ...args);
    },
    info: (message: string, ...args: any[]) => {
        console.info(`[INFO] ${message}`, ...args);
    },
    warn: (message: string, ...args: any[]) => {
        console.warn(`[WARN] ${message}`, ...args);
    },
    error: (error: Error | unknown, message: string, ...args: any[]) => {
        console.error(`[ERROR] ${message}`, error, ...args);
    },
};

// Create a fetch instance with the custom logger
const fetch = new FetchBuilder(
    z.object({
        id: z.string(),
        name: z.string(),
    }),
)
    .withLogger(customLogger)
    .build();

// All requests will now use your custom logger
const response = await fetch('https://api.example.com/users/123');

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

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.

Contact

Brent Rager

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

1.6.0

23 days ago

1.5.0

1 month ago

1.4.2

2 months ago

1.4.1

2 months ago

1.4.0

2 months ago

1.3.0

3 months ago

1.2.1

3 months ago

1.2.0

3 months ago

1.1.0

3 months ago

1.0.7

3 months ago

1.0.6

3 months ago

1.0.5

3 months ago