@autoagents/fetch
v0.1.4
Published
Backend communication layer with protocol enforcement and structured error handling
Downloads
485
Maintainers
Readme
@autoagents/fetch
Backend communication layer with structured error handling for autoagents projects.
What This Package Does
- Protocol Layer - Enforces
{ code, msg, data }response contract, auto-unwraps on success - Error System - HttpError (network/HTTP) vs ApiError (business logic) with full debug context
- RSC Support - Errors survive React Server Component serialization and can be fully reconstructed
- 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/ApiErrorhierarchy 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/fetchyarn add @autoagents/fetchpnpm add @autoagents/fetchbun add @autoagents/fetchDesign
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 + queryStringExample:
const fetch = createFetch({ baseUrl: 'https://api.example.com/v1' });
await fetch('/users', { params: { page: 1 } });
// → https://api.example.com/v1/users?page=1Runtime override:
await fetch('/weather', { baseUrl: 'https://external-api.com' });
// → https://external-api.com/weatherError System
Architecture
FetchError (abstract base)
├── HttpError ← HTTP failures (status ≠ 200)
├── ApiError ← Business failures (status = 200, code ≠ 1)
└── UnknownFetchError ← Fallback for unrecognized errorsHttpError 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→ returnsdatadirectlycode !== 1→ throwsApiErrorwithmsg
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: 0with descriptivemsg - [ ] HTTP 200 for business-level responses, 4xx/5xx for infrastructure failures
- [ ] Error bodies contain
errorormessagefield - [ ] Auth uses
Authorization: Bearerheader - [ ] Binary endpoints return raw data
