/safe-fetch

Tiny, type-safe fetch wrapper: safe results, retries, timeouts & validation

Primary LanguageTypeScriptMIT LicenseMIT

@asouei/safe-fetch

npm version CI License: MIT npm downloads TypeScript Bundle Size Zero Dependencies Open in CodeSandbox Awesome

English version | Русская версия

Never write try/catch for HTTP requests again. A complete ecosystem of type-safe HTTP utilities built around safe results and predictable error handling.

Modern HTTP client ecosystem that eliminates exceptions through discriminated unions, provides intelligent retries, handles timeouts properly, and integrates seamlessly with popular data fetching libraries.

📦 Packages

Package Version Description
@asouei/safe-fetch npm Core HTTP client with safe results, retries, and timeouts
@asouei/safe-fetch-react-query npm TanStack Query integration with optimized error handling

🚀 Quick Start

npm install @asouei/safe-fetch
import { safeFetch } from '@asouei/safe-fetch';

const result = await safeFetch.get<{ users: User[] }>('/api/users');
if (result.ok) {
  // TypeScript knows result.data is { users: User[] }
  console.log(result.data.users);
} else {
  // All errors are normalized and typed
  console.error(`${result.error.name}: ${result.error.message}`);
}

✨ Why safe-fetch?

  • 🛡️ No Exceptions: Never write try/catch — always get a safe result
  • 🔧 Typed Errors: NetworkError | TimeoutError | HttpError | ValidationError
  • ⏱️ Smart Timeouts: Per-attempt + total operation timeouts
  • 🔄 Intelligent Retries: Only retries safe operations + Retry-After support
  • 📦 Zero Dependencies: Tree-shakable, ~3kb, works everywhere
  • 🧪 Validation Ready: Built-in Zod integration without exceptions

📖 Documentation

🌟 Core Features

Safe Results

Every request returns a discriminated union - no more guessing what went wrong:

type SafeResult<T> = 
  | { ok: true; data: T; response: Response }
  | { ok: false; error: NormalizedError; response?: Response }

Normalized Errors

All errors follow the same structure:

// Network issues, connection failures  
type NetworkError = { name: 'NetworkError'; message: string; cause?: unknown }

// Request timeouts (per-attempt or total)
type TimeoutError = { name: 'TimeoutError'; message: string; timeoutMs: number }

// HTTP 4xx/5xx responses
type HttpError = { name: 'HttpError'; message: string; status: number; body?: unknown }

// Schema validation failures
type ValidationError = { name: 'ValidationError'; message: string; cause?: unknown }

Smart Configuration

import { createSafeFetch } from '@asouei/safe-fetch';

const api = createSafeFetch({
  baseURL: 'https://api.example.com',
  timeoutMs: 5000,        // Per attempt
  totalTimeoutMs: 30000,  // Total operation  
  retries: { 
    retries: 2,
    baseDelayMs: 300      // Exponential backoff
  },
  headers: { Authorization: 'Bearer token' }
});

const result = await api.get<User[]>('/users');

🔮 Ecosystem Roadmap

  • Core Library - Safe HTTP client with retries and timeouts
  • React Query Adapter - Optimized TanStack Query integration
  • 📋 SWR Adapter - SWR integration helpers
  • 🔍 ESLint Plugin - Enforce safe result patterns
  • 🏗️ Framework Examples - Next.js, Remix, Cloudflare Workers

📱 Framework Integration

React Query

import { createSafeFetch } from '@asouei/safe-fetch';
import { createQueryFn, rqDefaults } from '@asouei/safe-fetch-react-query';

const api = createSafeFetch({ baseURL: '/api' });
const queryFn = createQueryFn(api);

export function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: queryFn<User[]>('/users'),
    ...rqDefaults() // { retry: false } - let safe-fetch handle retries
  });
}

Next.js / SSR

// app/users/page.tsx
import { safeFetch } from '@asouei/safe-fetch';

export default async function UsersPage() {
  const result = await safeFetch.get<User[]>('/api/users');
  
  if (!result.ok) {
    return <ErrorPage error={result.error} />;
  }
  
  return <UserList users={result.data} />;
}

Cloudflare Workers

export default {
  async fetch(request: Request) {
    const result = await safeFetch.get<{ status: string }>('https://api.service.com/health');
    
    return new Response(
      result.ok ? JSON.stringify(result.data) : result.error.message,
      { status: result.ok ? 200 : 500 }
    );
  }
};

🤝 Contributing

We welcome contributions! Please see our Contributing Guide for details.

Quick development setup:

git clone https://github.com/asouei/safe-fetch.git
cd safe-fetch
pnpm install
pnpm -r test
pnpm -r build

📄 License

MIT © Aleksandr Mikhailishin


Built with ❤️ for developers who value predictable, type-safe HTTP clients.