{"_id":"@asouei/safe-fetch","_rev":"2-7fee43b5e77928b423bebb8e57569149","name":"@asouei/safe-fetch","dist-tags":{"latest":"1.0.0"},"versions":{"0.1.0":{"name":"@asouei/safe-fetch","version":"0.1.0","keywords":["fetch","safe","typescript","http","retry","timeout","error-handling","discriminated-union","zero-dependencies","type-safe"],"author":{"name":"Aleksandr Mikhailishin"},"license":"MIT","_id":"@asouei/safe-fetch@0.1.0","maintainers":[{"name":"asouei","email":"brain3run@gmail.com"}],"homepage":"https://asouei.dev","bugs":{"url":"https://github.com/asouei/safe-fetch/issues"},"url":"git+https://github.com/asouei/safe-fetch.git","dist":{"shasum":"72e20972284a21c6f1d92615a6c5e7438e402645","tarball":"https://registry.npmjs.org/@asouei/safe-fetch/-/safe-fetch-0.1.0.tgz","fileCount":16,"integrity":"sha512-JIi+L7mLSjLB4LgN7RBou89J6p6kAhau3mrdvvvM4wz5GpS1pE3pkfakAgXrvsjWBxLEv4cEPJRry8OtqI2IOw==","signatures":[{"sig":"MEQCIATgP5ZeLzgOEFpF+6K2ng6aTVgGkv8OQpgjc6FenwHOAiATM/HE/jXOXpp7KtdGrF6AyfXLdqBdLeUgusgVQzbfSg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":115704},"main":"dist/index.umd.cjs","type":"module","types":"dist/index.d.ts","module":"dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.umd.cjs"}},"gitHead":"7b9e6e557f1e9a3687335cb008396e52fe936c10","scripts":{"dev":"vite build --watch","lint":"eslint . --ext .ts","test":"vitest run","build":"vite build","clean":"rimraf dist","format":"prettier --write .","test:watch":"vitest","prepublishOnly":"npm run clean && npm run build && npm test"},"_npmUser":{"name":"asouei","email":"brain3run@gmail.com"},"repository":{"url":"git+https://github.com/asouei/safe-fetch.git","type":"git"},"_npmVersion":"11.5.2","description":"Tiny, typed wrapper around fetch with safe results, normalized errors, timeouts, retries and validation hooks.","directories":{},"sideEffects":false,"_nodeVersion":"22.17.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vite":"^6.0.0","jsdom":"^26.1.0","rimraf":"^6.0.1","vitest":"^3.2.4","typescript":"^5.9.0","@types/node":"^22.0.0","vite-plugin-dts":"^4.5.4"},"peerDependencies":{},"_npmOperationalInternal":{"tmp":"tmp/safe-fetch_0.1.0_1756720542398_0.044361408265648716","host":"s3://npm-registry-packages-npm-production"}},"1.0.0":{"name":"@asouei/safe-fetch","version":"1.0.0","description":"Tiny, typed wrapper around fetch with safe results, normalized errors, timeouts, retries and validation hooks.","type":"module","main":"dist/index.umd.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.umd.cjs"}},"sideEffects":false,"scripts":{"build":"vite build","dev":"vite build --watch","test":"vitest run","test:watch":"vitest","clean":"rimraf dist","prepublishOnly":"npm run clean && npm run build && npm test","lint":"echo lint:ok"},"keywords":["fetch","safe","typescript","http","retry","timeout","error-handling","discriminated-union","zero-dependencies","type-safe"],"author":{"name":"Aleksandr Mikhailishin"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/asouei/safe-fetch.git","directory":"packages/core"},"bugs":{"url":"https://github.com/asouei/safe-fetch/issues"},"homepage":"https://asouei.dev","publishConfig":{"access":"public"},"engines":{"node":">=18"},"devDependencies":{"@types/node":"^22.0.0","typescript":"^5.9.0","vite":"^6.0.0","vite-plugin-dts":"^4.5.4","vitest":"^3.2.4","jsdom":"^26.1.0","rimraf":"^6.0.1"},"peerDependencies":{},"_id":"@asouei/safe-fetch@1.0.0","gitHead":"13d4431a67fa5db4cc00356a37f5d37f3c10fce6","_nodeVersion":"22.17.0","_npmVersion":"11.5.2","dist":{"integrity":"sha512-KRo9btDUPQzHGb5W0YmQwyYTMFSI9dpgSTAJGb9Og42bcm2LvAR/dGbQsY5CaAhu4bXk1DUhDHKWTeqEBK+54A==","shasum":"7e2833dc06175eb2ef6502a2b6e439c45879fc64","tarball":"https://registry.npmjs.org/@asouei/safe-fetch/-/safe-fetch-1.0.0.tgz","fileCount":16,"unpackedSize":111921,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFm8nvIT0ULdtgFvacN3zP4Fu4erBgI45yPa9IuTdK4iAiEAjkkq0J0GDoE/JgKMfwgboAj7HKVgxMUlBvHtkO7KBXY="}]},"_npmUser":{"name":"asouei","email":"brain3run@gmail.com"},"directories":{},"maintainers":[{"name":"asouei","email":"brain3run@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/safe-fetch_1.0.0_1757060091395_0.4165145942059223"},"_hasShrinkwrap":false}},"time":{"created":"2025-09-01T09:55:42.228Z","modified":"2025-09-05T08:14:51.744Z","0.1.0":"2025-09-01T09:55:42.564Z","1.0.0":"2025-09-05T08:14:51.562Z"},"bugs":{"url":"https://github.com/asouei/safe-fetch/issues"},"author":{"name":"Aleksandr Mikhailishin"},"license":"MIT","homepage":"https://asouei.dev","keywords":["fetch","safe","typescript","http","retry","timeout","error-handling","discriminated-union","zero-dependencies","type-safe"],"repository":{"type":"git","url":"git+https://github.com/asouei/safe-fetch.git","directory":"packages/core"},"description":"Tiny, typed wrapper around fetch with safe results, normalized errors, timeouts, retries and validation hooks.","maintainers":[{"name":"asouei","email":"brain3run@gmail.com"}],"readme":"# @asouei/safe-fetch\r\n\r\n[![npm version](https://img.shields.io/npm/v/@asouei/safe-fetch.svg)](https://www.npmjs.com/package/@asouei/safe-fetch)\r\n[![CI](https://github.com/asouei/safe-fetch/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/asouei/safe-fetch/actions/workflows/ci.yml)\r\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\r\n[![npm downloads](https://img.shields.io/npm/dm/@asouei/safe-fetch)](https://www.npmjs.com/package/@asouei/safe-fetch)\r\n[![TypeScript](https://img.shields.io/badge/TypeScript-Ready-blue.svg)](https://www.typescriptlang.org/)\r\n[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@asouei/safe-fetch)](https://bundlephobia.com/package/@asouei/safe-fetch)\r\n[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-green.svg)](package.json)\r\n\r\n*[English version](README.md) | Русская версия*\r\n\r\n> **Никогда больше не пишите `try/catch` для HTTP-запросов.** Ноль зависимостей • Не бросает исключения • Полный таймаут • Поддержка Retry-After\r\n\r\nМаленькая, типизированная обертка вокруг `fetch`, которая возвращает безопасные результаты, умно обрабатывает таймауты и повторяет запросы с экспоненциальным отступом.\r\n\r\nЧасть **[экосистемы @asouei/safe-fetch](../../README.ru.md)** - также доступен: [адаптер React Query](../react-query).\r\n\r\n📌 Библиотека вошла в список [Awesome TypeScript](https://github.com/dzharii/awesome-typescript).\r\n\r\n```typescript\r\nimport { safeFetch } from '@asouei/safe-fetch';\r\n\r\nconst result = await safeFetch.get<{ users: User[] }>('/api/users');\r\nif (result.ok) {\r\n  // TypeScript знает, что result.data это { users: User[] }\r\n  console.log(result.data.users);\r\n} else {\r\n  // Все ошибки нормализованы - больше не нужно угадывать что пошло не так\r\n  console.error(result.error.name); // 'NetworkError' | 'TimeoutError' | 'HttpError' | 'ValidationError'\r\n}\r\n```\r\n\r\n## Что вы получаете\r\n\r\n- **Не бросает исключения:** Никогда не пишите `try/catch` — всегда получайте безопасный результат\r\n- **Типизированные ошибки:** `NetworkError | TimeoutError | HttpError | ValidationError`\r\n- **Двойные таймауты:** `timeoutMs` на попытку + `totalTimeoutMs` для всей операции\r\n- **Умные повторы:** Только идемпотентные методы по умолчанию + поддержка `Retry-After`\r\n- **Готовность к Zod:** Валидация схем без исключений\r\n- **Ноль зависимостей и ~3кб:** Дружелюбен к бандлерам, tree-shakable, без побочных эффектов\r\n\r\n| Функция | `@asouei/safe-fetch` | `axios` | `ky` | нативный `fetch` |\r\n|---------|---------------------|---------|------|------------------|\r\n| **Размер бандла** | ~3кб | ~13кб* | ~11кб* | 0кб |\r\n| **Зависимости** | 0 | 0* | 0* | 0 |\r\n| **Безопасные результаты (без исключений)** | ✅ | ❌ | ❌ | ❌ |\r\n| **Дискриминированные union типы** | ✅ | ❌ | ❌ | ❌ |\r\n| **Per-attempt + полный таймауты** | ✅ | Только на запрос | Только на запрос | Вручную |\r\n| **Умные повторы (только идемпотентные)** | ✅ | ✅ (бросает) | ✅ (бросает) | Вручную |\r\n| **Поддержка заголовка Retry-After** | ✅ | ❌ | ❌ | Вручную |\r\n| **Интерсепторы запроса/ответа** | ✅ | ✅ | ✅ | Вручную |\r\n| **Хуки валидации (готов к Zod)** | ✅ | ❌ | ❌ | Вручную |\r\n| **TypeScript-first дизайн** | ✅ | Частично | ✅ | ✅ |\r\n\r\n*Размер бандла ~gzip; зависит от версии, окружения и настроек бандлера.  \r\n**Axios/Ky бросают исключения на non-2xx по умолчанию; нет встроенного полного таймаута операции.\r\n\r\n## Установка\r\n\r\n```bash\r\nnpm install @asouei/safe-fetch\r\n```\r\n\r\n### Стили импорта\r\n\r\n**ESM**\r\n```typescript\r\nimport { safeFetch, createSafeFetch } from '@asouei/safe-fetch';\r\n```\r\n\r\n**CommonJS**\r\n```javascript\r\nconst { safeFetch, createSafeFetch } = require('@asouei/safe-fetch');\r\n// CommonJS поддерживается через поле exports.require\r\n```\r\n\r\n**CDN (esm.run)**\r\n```html\r\n<script type=\"module\">\r\n  import { safeFetch } from \"https://esm.run/@asouei/safe-fetch\";\r\n  const res = await safeFetch.get('/api/ping');\r\n</script>\r\n```\r\n\r\n## Быстрое демо\r\n\r\n```typescript\r\ntype Todo = { id: number; title: string; completed: boolean };\r\n\r\nconst api = createSafeFetch({\r\n  baseURL: 'https://jsonplaceholder.typicode.com',\r\n  timeoutMs: 3000,\r\n  totalTimeoutMs: 7000,\r\n  retries: { retries: 2 },\r\n});\r\n\r\nconst list = await api.get<Todo[]>('/todos', { query: { _limit: 3 } });\r\nif (list.ok) console.log('todos:', list.data.map(t => t.title));\r\n\r\nconst create = await api.post<Todo>('/todos', { title: 'Изучить safe-fetch', completed: false });\r\nif (!create.ok) console.warn('создание не удалось:', create.error);\r\n```\r\n\r\n## Парсинг JSON и обработка ошибок\r\n\r\n> **Поведение парсинга JSON:**\r\n> - Коды статуса `204/205` → `null`\r\n> - Если `Content-Type` не содержит `json` → `null`\r\n> - Невалидный JSON не бросает исключение, возвращает `null`\r\n\r\n**Типы ошибок, которые могут встретиться:** `NetworkError`, `TimeoutError`, `HttpError`, `ValidationError`.  \r\nВсе ошибки сериализуемы (обычные объекты), легко логировать и мониторить.\r\n\r\n**Поведение таймаута:**\r\n- `timeoutMs` — таймаут на попытку\r\n- `totalTimeoutMs` — таймаут всей операции (включает все повторы)\r\n\r\n**Tree-shakable, без побочных эффектов** - импортируете только то, что используете.\r\n\r\n### Безопасно по умолчанию\r\nБольше никаких блоков `try/catch`. Каждый запрос возвращает дискриминированное объединение:\r\n```typescript\r\ntype SafeResult<T> = \r\n  | { ok: true; data: T; response: Response }\r\n  | { ok: false; error: NormalizedError; response?: Response }\r\n```\r\n\r\n### Нормализованные типы ошибок\r\nВсе ошибки последовательно типизированы и структурированы:\r\n```typescript\r\n// Сетевые проблемы, сбои подключения\r\ntype NetworkError = { name: 'NetworkError'; message: string; cause?: unknown }\r\n\r\n// Таймауты запроса (на попытку или полный)\r\ntype TimeoutError = { name: 'TimeoutError'; message: string; timeoutMs: number }\r\n\r\n// HTTP 4xx/5xx ответы\r\ntype HttpError = { name: 'HttpError'; message: string; status: number; body?: unknown }\r\n\r\n// Сбои валидации схемы  \r\ntype ValidationError = { name: 'ValidationError'; message: string; cause?: unknown }\r\n```\r\n\r\n### Умные таймауты\r\nДвухуровневая система таймаутов для максимального контроля:\r\n```typescript\r\nconst api = createSafeFetch({\r\n  timeoutMs: 5000,        // 5с на попытку\r\n  totalTimeoutMs: 30000   // 30с всего (все повторы)\r\n});\r\n```\r\n\r\n### Умные повторы\r\nПо умолчанию повторяет только безопасные операции:\r\n- ✅ `GET`, `HEAD` - автоматически повторяются на 5xx, сетевых ошибках\r\n- ❌ `POST`, `PUT`, `PATCH` - никогда не повторяются по умолчанию (предотвращает дублирование)\r\n- 🎛️ Кастомный колбек `retryOn` для полного контроля\r\n\r\n```typescript\r\nconst result = await safeFetch.get('/api/flaky-endpoint', {\r\n  retries: {\r\n    retries: 3,\r\n    baseDelayMs: 300,     // Экспоненциальный отступ начиная с 300мс\r\n    retryOn: ({ response, error }) => {\r\n      // Кастомная логика повтора\r\n      return error?.name === 'NetworkError' || response?.status === 429;\r\n    }\r\n  }\r\n});\r\n```\r\n\r\n### Уважает лимиты скорости\r\nАвтоматически обрабатывает `429 Too Many Requests` с заголовком `Retry-After`:\r\n```typescript\r\n// Сервер возвращает: 429 Too Many Requests, Retry-After: 60\r\n// safe-fetch ждет ровно 60 секунд перед повтором\r\nconst result = await safeFetch.get('/api/rate-limited', {\r\n  retries: { retries: 3 }\r\n});\r\n```\r\n\r\n## Интеграция с фреймворками\r\n\r\n### React Query\r\n\r\n**Простая интеграция** с официальным адаптером:\r\n\r\n```bash\r\nnpm install @asouei/safe-fetch-react-query\r\n```\r\n\r\n```typescript\r\nimport { createSafeFetch } from '@asouei/safe-fetch';\r\nimport { createQueryFn, rqDefaults } from '@asouei/safe-fetch-react-query';\r\n\r\nconst api = createSafeFetch({ baseURL: '/api' });\r\nconst queryFn = createQueryFn(api);\r\n\r\nexport function useUsers() {\r\n  return useQuery({\r\n    queryKey: ['users'],\r\n    queryFn: queryFn<User[]>('/users'),\r\n    ...rqDefaults() // { retry: false } - пусть safe-fetch обрабатывает повторы\r\n  });\r\n}\r\n```\r\n\r\nСм. **[документацию адаптера React Query](../react-query)** для полного руководства по интеграции.\r\n\r\n### SWR\r\n\r\n```typescript\r\nimport useSWR from 'swr';\r\n\r\nconst fetcher = async (url: string) => {\r\n  const result = await safeFetch.get(url);\r\n  if (!result.ok) throw result.error;\r\n  return result.data;\r\n};\r\n\r\nexport function UserProfile({ id }: { id: string }) {\r\n  const { data, error } = useSWR(`/api/users/${id}`, fetcher);\r\n  if (error) return <div>Ошибка: {error.message}</div>;\r\n  if (!data) return <div>Загрузка...</div>;\r\n  return <div>Привет, {data.name}!</div>;\r\n}\r\n```\r\n\r\n## Миграция с Axios\r\n\r\n**Axios (бросает исключения)**\r\n```typescript\r\ntry {\r\n  const { data } = await axios.get<User[]>('/users');\r\n  render(data);\r\n} catch (e) {\r\n  toast(parseAxiosError(e));\r\n}\r\n```\r\n\r\n**safe-fetch (не бросает)**\r\n```typescript\r\nconst res = await safeFetch.get<User[]>('/users');\r\nif (res.ok) render(res.data);\r\nelse toast(`${res.error.name}: ${res.error.message}`);\r\n```\r\n\r\n## Примеры использования\r\n\r\n### Базовые запросы\r\n\r\n```typescript\r\nimport { safeFetch } from '@asouei/safe-fetch';\r\n\r\n// GET запрос с типобезопасностью\r\nconst users = await safeFetch.get<User[]>('/api/users');\r\nif (users.ok) {\r\n  users.data.forEach(user => console.log(user.name));\r\n}\r\n\r\n// POST с JSON телом (автоматически устанавливает Content-Type)\r\nconst newUser = await safeFetch.post('/api/users', {\r\n  name: 'Алиса',\r\n  email: 'alice@example.com'\r\n});\r\n\r\n// Обработка разных типов ошибок\r\nif (!newUser.ok) {\r\n  switch (newUser.error.name) {\r\n    case 'HttpError':\r\n      // Используем type assertion, так как знаем тип из дискриминированного объединения\r\n      const httpError = newUser.error as { status: number; message: string };\r\n      console.log(`HTTP ${httpError.status}: ${httpError.message}`);\r\n      break;\r\n    case 'NetworkError':\r\n      console.log('Сбой сетевого подключения');\r\n      break;\r\n    case 'TimeoutError':\r\n      const timeoutError = newUser.error as { timeoutMs: number };\r\n      console.log(`Запрос превысил время ожидания через ${timeoutError.timeoutMs}мс`);\r\n      break;\r\n    case 'ValidationError':\r\n      console.log('Валидация ответа не удалась');\r\n      break;\r\n  }\r\n}\r\n```\r\n\r\n### Настроенный экземпляр\r\n\r\n```typescript\r\nimport { createSafeFetch } from '@asouei/safe-fetch';\r\n\r\nconst api = createSafeFetch({\r\n  baseURL: 'https://api.example.com',\r\n  headers: { \r\n    'Authorization': 'Bearer token',\r\n    'User-Agent': 'MyApp/1.0'\r\n  },\r\n  timeoutMs: 8000,\r\n  totalTimeoutMs: 30000,\r\n  retries: { \r\n    retries: 2,\r\n    baseDelayMs: 500 \r\n  }\r\n});\r\n\r\n// Все запросы используют базовую конфигурацию\r\nconst result = await api.get('/users'); // GET https://api.example.com/users\r\n```\r\n\r\n### Валидация ответов с Zod\r\n\r\nИдеальная интеграция с библиотеками валидации схем:\r\n\r\n```typescript\r\nimport { z } from 'zod';\r\n\r\nconst UserSchema = z.object({\r\n  id: z.number(),\r\n  name: z.string(),\r\n  email: z.string().email()\r\n});\r\n\r\nconst validateWith = <T>(schema: z.ZodSchema<T>) => (raw: unknown) => {\r\n  const r = schema.safeParse(raw);\r\n  return r.success \r\n    ? { success: true as const, data: r.data } \r\n    : { success: false as const, error: r.error };\r\n};\r\n\r\nconst result = await safeFetch.get('/api/user/123', {\r\n  validate: validateWith(UserSchema)\r\n});\r\n\r\nif (result.ok) {\r\n  // result.data полностью типизирован как z.infer<typeof UserSchema>\r\n  console.log(result.data.email); // TypeScript знает, что это валидный email\r\n}\r\n```\r\n\r\n### Интерсепторы запроса/ответа\r\n\r\n```typescript\r\nconst api = createSafeFetch({\r\n  interceptors: {\r\n    onRequest: (url, init) => {\r\n      // Добавляем токен авторизации\r\n      const headers = new Headers(init.headers);\r\n      headers.set('Authorization', `Bearer ${getToken()}`);\r\n      init.headers = headers;\r\n      \r\n      console.log(`→ ${init.method} ${url}`);\r\n    },\r\n    \r\n    onResponse: (response) => {\r\n      console.log(`← ${response.status} ${response.url}`);\r\n      \r\n      // Обрабатываем глобальные ошибки авторизации\r\n      if (response.status === 401) {\r\n        redirectToLogin();\r\n      }\r\n    },\r\n    \r\n    onError: (error) => {\r\n      // Отправляем ошибки в сервис мониторинга\r\n      analytics.track('http_error', {\r\n        error_name: error.name,\r\n        message: error.message\r\n      });\r\n    }\r\n  }\r\n});\r\n```\r\n\r\n## FAQ\r\n\r\n**Почему не бросать исключения?**\r\nЯвный поток управления через `{ ok }` легче читать, типизировать и тестировать, чем try/catch вокруг каждой операции.\r\n\r\n**Можно ли все же бросать исключения при необходимости?**\r\nДа - используйте хелпер `unwrap(result)` из секции Утилиты.\r\n\r\n**Почему POST/PUT/PATCH не повторяются по умолчанию?**\r\nЧтобы предотвратить дублирование побочных эффектов. Включите повторы для неидемпотентных методов явно через колбек `retryOn`.\r\n\r\n**Работает ли это с React Query/SWR?**\r\nИдеально! Используйте наш [адаптер React Query](../react-query) или оберните ваши вызовы safeFetch хелпером `unwrap`.\r\n\r\n## Участие в разработке\r\n\r\nВклады приветствуются! Пожалуйста, прочитайте наш [Гид по участию](../../CONTRIBUTING.md) для подробностей.\r\n\r\n**Настройка разработки:**\r\n```bash\r\ngit clone https://github.com/asouei/safe-fetch.git\r\ncd safe-fetch/packages/core\r\npnpm install\r\npnpm test\r\npnpm build\r\n```\r\n\r\n## Лицензия\r\n\r\nMIT © [Aleksandr Mikhailishin](https://github.com/asouei)\r\n\r\n---\r\n\r\n**Сделано с ❤️ для разработчиков, которые ценят предсказуемые, типобезопасные HTTP клиенты.**","readmeFilename":"README.ru.md"}