Safe-fetch 1.0: от библиотеки к экосистеме за 72 часа
- суббота, 6 сентября 2025 г. в 00:00:03
Три дня назад я опубликовал статью про safe-fetch — библиотеку, которая убирает try/catch из HTTP-запросов. Вчера статья набрала 8,5K просмотров и 64 добавления в закладки. А сегодня представляю safe-fetch 1.0 — уже не просто библиотеку, а целую экосистему.
История о том, как фидбек сообщества за 72 часа превратил обертку над fetch в полноценную платформу для типобезопасного HTTP.
1 сентября выпустил safe-fetch 0.1.0 и написал статью. Идея простая: вместо try/catch везде — безопасные результаты с типизированными ошибками.
// Было
try {
const res = await fetch('/users');
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
} catch (e) {
console.error('Что-то сломалось:', e.message);
}
// Стало
const result = await safeFetch.get<User[]>('/users');
if (result.ok) {
console.log(result.data);
} else {
console.error(result.error.name); // NetworkError | TimeoutError | HttpError
}
4 сентября статья вышла на Хабре. 5 сентября — сегодня — выпускаю 1.0 с экосистемой.
8,5K просмотров за первые сутки
64 добавления в закладки
30 звезд на GitHub
70 скачиваний с NPM
2 пулл-реквеста от сообщества
Попадание в Awesome TypeScript
Комментарии были конкретными:
"Как интегрировать с React Query?"
"А для SWR будет?"
"Можно ли это организовать как монорепо?"
"Нужен ESLint plugin для проверки { ok }"
Стало понятно: одной библиотеки недостаточно. Нужна экосистема.
Первое изменение — переход на версию 1.0.0. Не новые фичи, а стабилизация архитектуры:
Переезд в workspace для управления несколькими пакетами:
packages/
├── core/ # @asouei/safe-fetch
└── react-query/ # @asouei/safe-fetch-react-query
Vite + Vitest вместо custom build
GitHub Actions для всех пакетов сразу
ESM/CJS экспорты улучшены
Весь существующий код работает без изменений:
const result = await safeFetch.get('/api/data');
// Точно так же, как в 0.1.0
Самый частый вопрос касался React Query. Проблема понятна: React Query ожидает throws, safe-fetch возвращает { ok: false }
.
В React Query принято бросать ошибки для их обработки:
const { data, error } = useQuery({
queryFn: async () => {
const response = await fetch('/users');
if (!response.ok) throw new Error('Failed');
return response.json();
}
});
С safe-fetch приходилось делать unwrap вручную:
const { data, error } = useQuery({
queryFn: async () => {
const result = await safeFetch.get<User[]>('/users');
if (!result.ok) throw result.error; // каждый раз вручную
return result.data;
},
retry: false // не забыть отключить
});
Встречайте @asouei/safe-fetch-react-query@0.1.0
:
npm install @asouei/safe-fetch-react-query
import { createQueryFn, rqDefaults } from '@asouei/safe-fetch-react-query';
const queryFn = createQueryFn(api);
const { data, error } = useQuery({
queryKey: ['users'],
queryFn: queryFn<User[]>('/users'),
...rqDefaults() // { retry: false } + другие настройки
});
// error теперь typed: NetworkError | HttpError | TimeoutError | ValidationError
Автоматический unwrap: { ok: false }
→ throw error
Сохранение типизации: TypeScript знает тип ошибки в React Query
Фабричные функции: createQueryFn
, createMutationFn
Умные дефолты: rqDefaults()
отключает дублирующие ретраи
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: async () => {
const result = await safeFetch.get<User[]>('/users');
if (!result.ok) throw result.error; // копипаста
return result.data;
},
retry: false // можно забыть
});
}
function useCreateUser() {
return useMutation({
mutationFn: async (data: NewUser) => {
const result = await safeFetch.post<User>('/users', data);
if (!result.ok) throw result.error; // опять
return result.data;
}
});
}
const api = createSafeFetch({ baseURL: '/api' });
const queryFn = createQueryFn(api);
const mutationFn = createMutationFn(api);
function useUsers() {
return useQuery({
queryKey: ['users'],
queryFn: queryFn<User[]>('/users'),
...rqDefaults()
});
}
function useCreateUser() {
return useMutation({
mutationFn: mutationFn<User>('/users', { method: 'POST' })
});
}
Результат: 45 строк unwrap кода → 0 строк. Время рефакторинга: 20 минут.
Тестировал адаптер на реальном проекте. Было 15 хуков с React Query, каждый с одинаковым unwrap паттерном.
Проблемы до адаптера:
8 раз забыл retry: false
— получал двойные ретраи
3 раза TypeScript не поймал ошибку типизации
45 строк дублирующегося кода
После внедрения:
0 забытых настроек — rqDefaults()
всегда одинаковые
Полная типизация ошибок работает из коробки
Вся логика в фабриках — единая точка истины
Safe-fetch больше не просто библиотека. Это платформа для типобезопасного HTTP:
Модульность: каждый адаптер — отдельный пакет
Типобезопасность: TypeScript first во всем
Zero config: работает из коробки с умными дефолтами
Обратная совместимость: апдейты не ломают код
Критерий | safe-fetch ecosystem | axios + react-query | ky + react-query |
---|---|---|---|
Безопасные результаты | ✅ Встроено | ❌ Исключения | ❌ Исключения |
Типизированные ошибки | ✅ В React Query тоже | ❌ Нет | ❌ Нет |
Общий таймаут операции | ✅ totalTimeoutMs | ❌ Только на запрос | ❌ Только на запрос |
Retry-After поддержка | ✅ Автоматически | ❌ Вручную | ❌ Вручную |
Unwrap boilerplate | ✅ Адаптер решает | ❌ В каждом хуке | ❌ В каждом хуке |
Bundle size | ~3kb + ~0.5kb | ~13kb | ~11kb |
npm install @asouei/safe-fetch @asouei/safe-fetch-react-query
import { createSafeFetch } from '@asouei/safe-fetch';
import { createQueryFn, createMutationFn, rqDefaults } from '@asouei/safe-fetch-react-query';
const api = createSafeFetch({
baseURL: '/api',
timeoutMs: 5000,
retries: { retries: 2 }
});
const queryFn = createQueryFn(api);
const mutationFn = createMutationFn(api);
function UserProfile({ userId }: { userId: string }) {
const { data: user, error, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: queryFn<User>(`/users/${userId}`),
...rqDefaults()
});
const updateUser = useMutation({
mutationFn: mutationFn<User>(`/users/${userId}`, { method: 'PUT' })
});
if (isLoading) return <div>Loading...</div>;
if (error) {
// error typed as NetworkError | HttpError | TimeoutError | ValidationError
return <div>Error: {error.name} - {error.message}</div>;
}
return (
<div>
<h1>{user?.name}</h1>
<button onClick={() => updateUser.mutate({ name: 'New Name' })}>
Update
</button>
</div>
);
}
SWR адаптер — аналогично React Query (0.x experimental)
Next.js стартер — готовый шаблон проекта
Cloudflare Workers примеры — для edge computing
ESLint plugin — правила для паттерна { ok }
VS Code extension — сниппеты и автодополнение
Chrome DevTools — инспектирование safe-fetch запросов
OpenAPI генератор — typed клиенты из схем
GraphQL мост — safe results для GraphQL
Request deduplication — автоматическая дедупликация запросов
За первый день собрался интересный фидбек:
"Надо будет "потыкать", так как хочется предложить альтернативу axios'у в проектах." — техлид из e-commerce
"Попробовал на pet проекте. Больше не хочу возвращаться к обычному fetch" — фронтенд разработчик
Если уже используете TanStack React Query и цените типизацию — адаптер решает все проблемы интеграции.
Комбинация safe-fetch (надежные таймауты + ретраи) + React Query (кеширование) = bulletproof архитектура.
Фабричный подход унифицирует HTTP в команде. Джуниоры физически не смогут написать неправильный код.
За 72 часа произошла эволюция:
1 сентября: идея и релиз 0.1.0
4 сентября: статья на Хабре, фидбек сообщества
5 сентября: экосистема 1.0 с React Query адаптером
Safe-fetch core 1.0 стабилен и готов к production. React Query adapter 0.1 — экспериментальный, но уже снимает весь unwrap-бойлерплейт. Попробуйте, киньте фидбек в Issues и поставьте звезду на GitHub — это реально помогает проекту расти.
Следующая статья будет про SWR адаптер. Подписывайтесь на эволюцию экосистемы в реальном времени.
Ссылки: