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

@asouei/safe-fetch

v1.0.0

Published

Tiny, typed wrapper around fetch with safe results, normalized errors, timeouts, retries and validation hooks.

Readme

@asouei/safe-fetch

npm version CI License: MIT npm downloads TypeScript Bundle Size Zero Dependencies

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

Никогда больше не пишите try/catch для HTTP-запросов. Ноль зависимостей • Не бросает исключения • Полный таймаут • Поддержка Retry-After

Маленькая, типизированная обертка вокруг fetch, которая возвращает безопасные результаты, умно обрабатывает таймауты и повторяет запросы с экспоненциальным отступом.

Часть экосистемы @asouei/safe-fetch - также доступен: адаптер React Query.

📌 Библиотека вошла в список Awesome TypeScript.

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

const result = await safeFetch.get<{ users: User[] }>('/api/users');
if (result.ok) {
  // TypeScript знает, что result.data это { users: User[] }
  console.log(result.data.users);
} else {
  // Все ошибки нормализованы - больше не нужно угадывать что пошло не так
  console.error(result.error.name); // 'NetworkError' | 'TimeoutError' | 'HttpError' | 'ValidationError'
}

Что вы получаете

  • Не бросает исключения: Никогда не пишите try/catch — всегда получайте безопасный результат
  • Типизированные ошибки: NetworkError | TimeoutError | HttpError | ValidationError
  • Двойные таймауты: timeoutMs на попытку + totalTimeoutMs для всей операции
  • Умные повторы: Только идемпотентные методы по умолчанию + поддержка Retry-After
  • Готовность к Zod: Валидация схем без исключений
  • Ноль зависимостей и ~3кб: Дружелюбен к бандлерам, tree-shakable, без побочных эффектов

| Функция | @asouei/safe-fetch | axios | ky | нативный fetch | |---------|---------------------|---------|------|------------------| | Размер бандла | ~3кб | ~13кб* | ~11кб* | 0кб | | Зависимости | 0 | 0* | 0* | 0 | | Безопасные результаты (без исключений) | ✅ | ❌ | ❌ | ❌ | | Дискриминированные union типы | ✅ | ❌ | ❌ | ❌ | | Per-attempt + полный таймауты | ✅ | Только на запрос | Только на запрос | Вручную | | Умные повторы (только идемпотентные) | ✅ | ✅ (бросает) | ✅ (бросает) | Вручную | | Поддержка заголовка Retry-After | ✅ | ❌ | ❌ | Вручную | | Интерсепторы запроса/ответа | ✅ | ✅ | ✅ | Вручную | | Хуки валидации (готов к Zod) | ✅ | ❌ | ❌ | Вручную | | TypeScript-first дизайн | ✅ | Частично | ✅ | ✅ |

*Размер бандла ~gzip; зависит от версии, окружения и настроек бандлера.
**Axios/Ky бросают исключения на non-2xx по умолчанию; нет встроенного полного таймаута операции.

Установка

npm install @asouei/safe-fetch

Стили импорта

ESM

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

CommonJS

const { safeFetch, createSafeFetch } = require('@asouei/safe-fetch');
// CommonJS поддерживается через поле exports.require

CDN (esm.run)

<script type="module">
  import { safeFetch } from "https://esm.run/@asouei/safe-fetch";
  const res = await safeFetch.get('/api/ping');
</script>

Быстрое демо

type Todo = { id: number; title: string; completed: boolean };

const api = createSafeFetch({
  baseURL: 'https://jsonplaceholder.typicode.com',
  timeoutMs: 3000,
  totalTimeoutMs: 7000,
  retries: { retries: 2 },
});

const list = await api.get<Todo[]>('/todos', { query: { _limit: 3 } });
if (list.ok) console.log('todos:', list.data.map(t => t.title));

const create = await api.post<Todo>('/todos', { title: 'Изучить safe-fetch', completed: false });
if (!create.ok) console.warn('создание не удалось:', create.error);

Парсинг JSON и обработка ошибок

Поведение парсинга JSON:

  • Коды статуса 204/205null
  • Если Content-Type не содержит jsonnull
  • Невалидный JSON не бросает исключение, возвращает null

Типы ошибок, которые могут встретиться: NetworkError, TimeoutError, HttpError, ValidationError.
Все ошибки сериализуемы (обычные объекты), легко логировать и мониторить.

Поведение таймаута:

  • timeoutMs — таймаут на попытку
  • totalTimeoutMs — таймаут всей операции (включает все повторы)

Tree-shakable, без побочных эффектов - импортируете только то, что используете.

Безопасно по умолчанию

Больше никаких блоков try/catch. Каждый запрос возвращает дискриминированное объединение:

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

Нормализованные типы ошибок

Все ошибки последовательно типизированы и структурированы:

// Сетевые проблемы, сбои подключения
type NetworkError = { name: 'NetworkError'; message: string; cause?: unknown }

// Таймауты запроса (на попытку или полный)
type TimeoutError = { name: 'TimeoutError'; message: string; timeoutMs: number }

// HTTP 4xx/5xx ответы
type HttpError = { name: 'HttpError'; message: string; status: number; body?: unknown }

// Сбои валидации схемы  
type ValidationError = { name: 'ValidationError'; message: string; cause?: unknown }

Умные таймауты

Двухуровневая система таймаутов для максимального контроля:

const api = createSafeFetch({
  timeoutMs: 5000,        // 5с на попытку
  totalTimeoutMs: 30000   // 30с всего (все повторы)
});

Умные повторы

По умолчанию повторяет только безопасные операции:

  • GET, HEAD - автоматически повторяются на 5xx, сетевых ошибках
  • POST, PUT, PATCH - никогда не повторяются по умолчанию (предотвращает дублирование)
  • 🎛️ Кастомный колбек retryOn для полного контроля
const result = await safeFetch.get('/api/flaky-endpoint', {
  retries: {
    retries: 3,
    baseDelayMs: 300,     // Экспоненциальный отступ начиная с 300мс
    retryOn: ({ response, error }) => {
      // Кастомная логика повтора
      return error?.name === 'NetworkError' || response?.status === 429;
    }
  }
});

Уважает лимиты скорости

Автоматически обрабатывает 429 Too Many Requests с заголовком Retry-After:

// Сервер возвращает: 429 Too Many Requests, Retry-After: 60
// safe-fetch ждет ровно 60 секунд перед повтором
const result = await safeFetch.get('/api/rate-limited', {
  retries: { retries: 3 }
});

Интеграция с фреймворками

React Query

Простая интеграция с официальным адаптером:

npm install @asouei/safe-fetch-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 } - пусть safe-fetch обрабатывает повторы
  });
}

См. документацию адаптера React Query для полного руководства по интеграции.

SWR

import useSWR from 'swr';

const fetcher = async (url: string) => {
  const result = await safeFetch.get(url);
  if (!result.ok) throw result.error;
  return result.data;
};

export function UserProfile({ id }: { id: string }) {
  const { data, error } = useSWR(`/api/users/${id}`, fetcher);
  if (error) return <div>Ошибка: {error.message}</div>;
  if (!data) return <div>Загрузка...</div>;
  return <div>Привет, {data.name}!</div>;
}

Миграция с Axios

Axios (бросает исключения)

try {
  const { data } = await axios.get<User[]>('/users');
  render(data);
} catch (e) {
  toast(parseAxiosError(e));
}

safe-fetch (не бросает)

const res = await safeFetch.get<User[]>('/users');
if (res.ok) render(res.data);
else toast(`${res.error.name}: ${res.error.message}`);

Примеры использования

Базовые запросы

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

// GET запрос с типобезопасностью
const users = await safeFetch.get<User[]>('/api/users');
if (users.ok) {
  users.data.forEach(user => console.log(user.name));
}

// POST с JSON телом (автоматически устанавливает Content-Type)
const newUser = await safeFetch.post('/api/users', {
  name: 'Алиса',
  email: '[email protected]'
});

// Обработка разных типов ошибок
if (!newUser.ok) {
  switch (newUser.error.name) {
    case 'HttpError':
      // Используем type assertion, так как знаем тип из дискриминированного объединения
      const httpError = newUser.error as { status: number; message: string };
      console.log(`HTTP ${httpError.status}: ${httpError.message}`);
      break;
    case 'NetworkError':
      console.log('Сбой сетевого подключения');
      break;
    case 'TimeoutError':
      const timeoutError = newUser.error as { timeoutMs: number };
      console.log(`Запрос превысил время ожидания через ${timeoutError.timeoutMs}мс`);
      break;
    case 'ValidationError':
      console.log('Валидация ответа не удалась');
      break;
  }
}

Настроенный экземпляр

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

const api = createSafeFetch({
  baseURL: 'https://api.example.com',
  headers: { 
    'Authorization': 'Bearer token',
    'User-Agent': 'MyApp/1.0'
  },
  timeoutMs: 8000,
  totalTimeoutMs: 30000,
  retries: { 
    retries: 2,
    baseDelayMs: 500 
  }
});

// Все запросы используют базовую конфигурацию
const result = await api.get('/users'); // GET https://api.example.com/users

Валидация ответов с Zod

Идеальная интеграция с библиотеками валидации схем:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email()
});

const validateWith = <T>(schema: z.ZodSchema<T>) => (raw: unknown) => {
  const r = schema.safeParse(raw);
  return r.success 
    ? { success: true as const, data: r.data } 
    : { success: false as const, error: r.error };
};

const result = await safeFetch.get('/api/user/123', {
  validate: validateWith(UserSchema)
});

if (result.ok) {
  // result.data полностью типизирован как z.infer<typeof UserSchema>
  console.log(result.data.email); // TypeScript знает, что это валидный email
}

Интерсепторы запроса/ответа

const api = createSafeFetch({
  interceptors: {
    onRequest: (url, init) => {
      // Добавляем токен авторизации
      const headers = new Headers(init.headers);
      headers.set('Authorization', `Bearer ${getToken()}`);
      init.headers = headers;
      
      console.log(`→ ${init.method} ${url}`);
    },
    
    onResponse: (response) => {
      console.log(`← ${response.status} ${response.url}`);
      
      // Обрабатываем глобальные ошибки авторизации
      if (response.status === 401) {
        redirectToLogin();
      }
    },
    
    onError: (error) => {
      // Отправляем ошибки в сервис мониторинга
      analytics.track('http_error', {
        error_name: error.name,
        message: error.message
      });
    }
  }
});

FAQ

Почему не бросать исключения? Явный поток управления через { ok } легче читать, типизировать и тестировать, чем try/catch вокруг каждой операции.

Можно ли все же бросать исключения при необходимости? Да - используйте хелпер unwrap(result) из секции Утилиты.

Почему POST/PUT/PATCH не повторяются по умолчанию? Чтобы предотвратить дублирование побочных эффектов. Включите повторы для неидемпотентных методов явно через колбек retryOn.

Работает ли это с React Query/SWR? Идеально! Используйте наш адаптер React Query или оберните ваши вызовы safeFetch хелпером unwrap.

Участие в разработке

Вклады приветствуются! Пожалуйста, прочитайте наш Гид по участию для подробностей.

Настройка разработки:

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

Лицензия

MIT © Aleksandr Mikhailishin


Сделано с ❤️ для разработчиков, которые ценят предсказуемые, типобезопасные HTTP клиенты.