npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@autoagents/fetch

v0.1.4

Published

Backend communication layer with protocol enforcement and structured error handling

Downloads

485

Readme

@autoagents/fetch

Backend communication layer with structured error handling for autoagents projects.

What This Package Does

  1. Protocol Layer - Enforces { code, msg, data } response contract, auto-unwraps on success
  2. Error System - HttpError (network/HTTP) vs ApiError (business logic) with full debug context
  3. RSC Support - Errors survive React Server Component serialization and can be fully reconstructed
  4. Error Utilities - Type guards, parsers, and debug tools for consistent error handling

Features

API Design

  • 🪶 Fetch-like: Familiar API, minimal learning curve
  • 🔧 Configurable: Project-level defaults + per-request overrides
  • 🎯 Type-safe: Full TypeScript support with generic types

Protocol & Error Handling

  • 📡 Protocol enforcement: Auto-unwrap { code, msg, data } format, throw on business errors
  • 🚨 Structured errors: HttpError / ApiError hierarchy with debug context (URL, headers, curl)
  • ⚛️ RSC-ready: Errors serialize/deserialize across server-client boundary
  • 🔧 Error utilities: Type guards (isHttpError), parsers (FetchError.from), debug helpers

Built-in

  • 🔐 Auth: Token injection and refresh handling

Installation

npm install @autoagents/fetch
yarn add @autoagents/fetch
pnpm add @autoagents/fetch
bun add @autoagents/fetch

Design

Configure once, use everywhere.

createFetch() returns a pre-configured fetch instance - separating "how to request" (config) from "what to request" (business logic).

Quick Start

// lib/fetch.ts - Fetch wrapper (global config + error handling)
import { createFetch, FetchError } from '@autoagents/fetch';

const baseFetch = createFetch({
    baseUrl: '/api',
    getToken: () => localStorage.getItem('token'),
    setToken: (token) => localStorage.setItem('token', token), // Auto-refresh when server returns x-new-token header
});

// Wrap with global error handling
async function fetchWithErrorHandling<T>(...args: Parameters<typeof baseFetch>): Promise<T> {
    try {
        return await baseFetch<T>(...args);
    } catch (e) {
        const err = baseFetch.parseError(e);
        
        // Global error handling
        if (baseFetch.isHttpError(err) && err.status === 401) {
            window.location.href = '/login';
        }
        
        toast.error(err.userMessage);
        throw err;
    }
}

// Attach utilities to fetch function
export const fetch = Object.assign(fetchWithErrorHandling, {
    isHttpError: baseFetch.isHttpError,
    isApiError: baseFetch.isApiError,
    parseError: baseFetch.parseError,
});
// api-client/user.ts
import { fetch } from '@/lib/fetch';

export const user = {
    getProfile: () => fetch<User>('/user/profile'),
    update: (data: UpdateUserInput) => fetch<User>('/user', { method: 'POST', body: data }),
};
// api-client/project.ts
import { fetch } from '@/lib/fetch';

export const project = {
    list: () => fetch<Project[]>('/projects'),
    get: (id: string) => fetch<Project>(`/projects/${id}`),
    create: (data: CreateProjectInput) => fetch<Project>('/projects', { method: 'POST', body: data }),
};
// api-client/index.ts - API wrapper (aggregate all modules)
import { user } from './user';
import { project } from './project';

export const api = { user, project };
// Use in components
import { api } from '@/api-client';

const user = await api.user.getProfile();
const projects = await api.project.list();

API Reference

createFetch(config)

Creates a configured fetch instance.

interface FetchConfig {
    baseUrl?: string;       // Server origin + path prefix (e.g., 'https://api.example.com/v1' or '/api')
    timeout?: number;       // Request timeout in milliseconds (default: 60000)
    headers?: Record<string, string>;  // Default headers
    getToken?: () => string | null;    // Token getter function
    setToken?: (token: string) => void; // Token setter (called when x-new-token header received)
}

Fetch Instance Methods

| Method | Description | |--------|-------------| | fetch<T>(url, options?) | Make a typed request | | fetch.isHttpError(e) | Type guard for HTTP/network errors | | fetch.isApiError(e) | Type guard for business logic errors | | fetch.isFetchError(e) | Type guard for any fetch error | | fetch.parseError(e) | Parse any error into FetchError (RSC-safe) |

Request Options

interface FetchOptions {
    method?: string;           // HTTP method (default: 'GET')
    body?: unknown;            // Request body (auto JSON.stringify)
    params?: Record<string, string | number>;  // Query parameters
    headers?: Record<string, string>;          // Additional headers
    token?: string | null;     // Override token for this request
    baseUrl?: string;          // Override baseUrl for this request
    responseType?: 'json' | 'blob' | 'arraybuffer' | 'text';  // Response type (default: 'json')
}

Error Classes

| Class | When Thrown | Key Properties | |-------|-------------|----------------| | HttpError | HTTP status ≠ 200, network error | status, statusText, url, method, curl | | ApiError | HTTP 200 but code ≠ 1 | code, url, method, response | | UnknownFetchError | Fallback for unrecognized errors | message |

Business Codes (ApiCode)

enum ApiCode {
    SUCCESS = 1,        // Operation succeeded
    ERROR = 0,          // Generic business error
    UNAUTHORIZED = 401, // Token invalid/expired
    FORBIDDEN = 403,    // No permission
    CONFLICT = 409,     // Resource conflict
}

Usage Examples

Making Requests

// GET request
const user = await fetch<User>('/user/profile');

// POST request with body
const result = await fetch<Result>('/user/update', {
    method: 'POST',
    body: { name: 'John', email: '[email protected]' },
});

// Request with query parameters
const list = await fetch<Item[]>('/items', {
    params: { page: 1, limit: 20 },
});

// Download file
const blob = await fetch<Blob>('/download/file.pdf', { responseType: 'blob' });

File Upload

import { createFetch, createUpload } from '@autoagents/fetch';

const fetch = createFetch({ baseUrl: '/api' });
const upload = createUpload(fetch);

const response = await upload('/upload', file);

Token Management

// From localStorage
const fetch = createFetch({
    getToken: () => {
        if (typeof window === 'undefined') return null;
        return localStorage.getItem('your-token-key');
    },
});

// From cookie
const fetch = createFetch({
    getToken: () => getCookie('auth_token'),
});

// From store
const fetch = createFetch({
    getToken: () => authStore.getToken(),
});

URL Construction

Final URL = baseUrl + endpoint + queryString

Example:

const fetch = createFetch({ baseUrl: 'https://api.example.com/v1' });

await fetch('/users', { params: { page: 1 } });
// → https://api.example.com/v1/users?page=1

Runtime override:

await fetch('/weather', { baseUrl: 'https://external-api.com' });
// → https://external-api.com/weather

Error System

Architecture

FetchError (abstract base)
├── HttpError        ← HTTP failures (status ≠ 200)
├── ApiError         ← Business failures (status = 200, code ≠ 1)
└── UnknownFetchError ← Fallback for unrecognized errors

HttpError Triggers

| Scenario | Status | Example | |----------|--------|---------| | Network Error | 0 | CORS, connection refused, abort | | HTTP Non-200 | Response status | 404, 500, 502 | | Response Read Failure | 500 | Stream interrupted | | JSON Parse Failure | 500 | Server returns HTML |

Error Properties

| Property | HttpError | ApiError | Description | |----------|:-----------:|:----------:|-------------| | status | ✅ | - | HTTP status code | | statusText | ✅ | - | HTTP status text | | code | - | ✅ | Business error code | | url | ✅ | ✅ | Request URL | | method | ✅ | ✅ | HTTP method | | response | ✅ | ✅ | Response body | | curl | ✅ | - | cURL command for debugging | | userMessage | ✅ | ✅ | Clean message for UI display |

RSC Serialization

React Server Components only preserve name and message when passing errors to Client Components. This package embeds debug data in the message:

User-friendly message
__FETCH_ERROR__:{"status":404,"url":"/api/user",...}

Use FetchError.from(error) to reconstruct full error on client:

// app/error.tsx (Client Component)
'use client';

export default function ErrorPage({ error }: { error: Error }) {
    const fetchError = fetch.parseError(error);
    
    if (fetch.isHttpError(fetchError) && fetchError.status === 401) {
        redirect('/login');
    }
    
    return <h1>{fetchError.userMessage}</h1>;
}

⚠️ message vs userMessage

// ❌ DON'T - contains debug JSON
toast.error(error.message);

// ✅ DO - clean user-friendly message
toast.error(error.userMessage);

Backend Contract

Response Format

All JSON API responses MUST follow this structure:

interface Container<T> {
    code: ApiCode;   // Business status code (required)
    msg: string;     // Human-readable message (required)
    data: T;         // Actual payload (required, can be null)
}

Behavior:

  • code === 1 → returns data directly
  • code !== 1 → throws ApiError with msg

HTTP vs Business Code

HTTP Status Code
├── Non-200 (4xx, 5xx) → HttpError
└── 200 OK
    └── Business Code in Body
        ├── code=1 → Success (return data)
        └── code≠1 → ApiError (throw)

Convention: Use HTTP 200 for all requests that reach business logic. Use 4xx/5xx only for infrastructure failures.

HTTP Error Response

When returning non-200 status, include error message in body:

{ "error": "Detailed error description" }

Parse order: json.error || json.message || json.msg || json.errorMessage

Authentication

Request: Authorization: Bearer {token}

Token Refresh: Backend can send new token via x-new-token header.

Binary Responses

When responseType: 'blob' | 'arraybuffer' | 'text', return raw data directly (no Container wrapper).

Checklist for Backend

  • [ ] All JSON responses use { code, msg, data } format
  • [ ] Success = code: 1, Business error = code: 0 with descriptive msg
  • [ ] HTTP 200 for business-level responses, 4xx/5xx for infrastructure failures
  • [ ] Error bodies contain error or message field
  • [ ] Auth uses Authorization: Bearer header
  • [ ] Binary endpoints return raw data