{"_id":"@bonhomie/fetch-kit","name":"@bonhomie/fetch-kit","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@bonhomie/fetch-kit","version":"1.0.0","description":"Smart fetch wrapper for React and Node: auth token injection, token refresh on 401, retry with backoff, timeout, interceptors, and consistent errors.","type":"module","main":"dist/index.cjs","module":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":"./dist/index.js","require":"./dist/index.cjs"}},"sideEffects":false,"scripts":{"build":"tsup src/index.js --format esm,cjs --dts","prepublishOnly":"npm run build","test":"node --test test/fetch-kit.test.js"},"keywords":["fetch","http","api-client","axios-alternative","fetch-wrapper","retry","timeout","token-refresh","interceptors","react","node","browser","auth","jwt","api","rest","bonhomie"],"author":{"name":"Bonhomie"},"license":"MIT","devDependencies":{"tsup":"^8.5.1","typescript":"^5.9.3"},"_id":"@bonhomie/fetch-kit@1.0.0","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-e/mikMvGgtil9fm0RdCPXcnw69BRu52/hp6QCkJJMB+e0lNv5JnknL3c6rC6iS7InngitWYPD1eHNAF4lcwiGg==","shasum":"f431c7b443a75e7515c5ee35fbff747d6f20d7a3","tarball":"https://registry.npmjs.org/@bonhomie/fetch-kit/-/fetch-kit-1.0.0.tgz","fileCount":6,"unpackedSize":47727,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCczDETl5pCipNi4ZHIPJn2i3ati7pJRgt7ml/0niT7mwIgcxoQHYLTNT7gNlvzh4LeszEUbdy4XTwK148fNy1VB6U="}]},"_npmUser":{"name":"bonhomie95","email":"adeyemibabatundejoseph@gmail.com"},"directories":{},"maintainers":[{"name":"bonhomie95","email":"adeyemibabatundejoseph@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fetch-kit_1.0.0_1777496557654_0.5550806741420389"},"_hasShrinkwrap":false}},"time":{"created":"2026-04-29T21:02:37.565Z","1.0.0":"2026-04-29T21:02:37.810Z","modified":"2026-04-29T21:02:38.026Z"},"maintainers":[{"name":"bonhomie95","email":"adeyemibabatundejoseph@gmail.com"}],"description":"Smart fetch wrapper for React and Node: auth token injection, token refresh on 401, retry with backoff, timeout, interceptors, and consistent errors.","keywords":["fetch","http","api-client","axios-alternative","fetch-wrapper","retry","timeout","token-refresh","interceptors","react","node","browser","auth","jwt","api","rest","bonhomie"],"author":{"name":"Bonhomie"},"license":"MIT","readme":"# @bonhomie/fetch-kit\n\nSmart fetch wrapper for React and Node. Drop-in `axios` alternative — smaller, modern, and built around native `fetch`.\n\n![npm](https://img.shields.io/npm/v/@bonhomie/fetch-kit)\n![license](https://img.shields.io/npm/l/@bonhomie/fetch-kit)\n![zero deps](https://img.shields.io/badge/dependencies-0-brightgreen)\n\n---\n\n## Why not axios?\n\n- **axios** is 400KB+, wraps XMLHttpRequest, and has an outdated API.\n- **fetch-kit** wraps native `fetch`, works in browsers and Node 18+, and covers everything you actually need from axios.\n\n---\n\n## 📦 Installation\n\n```bash\nnpm install @bonhomie/fetch-kit\n```\n\nRequires native `fetch` — built into browsers and Node 18+. No polyfill needed.\n\n---\n\n## Quick Start\n\n```js\nimport { createFetchKit } from \"@bonhomie/fetch-kit\";\n\nconst api = createFetchKit({\n  baseUrl: \"https://api.yourapp.com\",\n  getToken: () => localStorage.getItem(\"token\"),\n  onTokenExpired: async () => {\n    const { token } = await refreshToken();\n    localStorage.setItem(\"token\", token);\n    return token;\n  },\n  retries: 2,\n  timeout: 8000,\n});\n\nconst users = await api.get(\"/users\");\nconst user  = await api.post(\"/users\", { name: \"Alice\" });\nawait       api.put(\"/users/1\", { name: \"Bob\" });\nawait       api.patch(\"/users/1\", { active: false });\nawait       api.delete(\"/users/1\");\n```\n\n---\n\n## Configuration\n\n| Option           | Type                        | Default           | Description                                                    |\n| ---------------- | --------------------------- | ----------------- | -------------------------------------------------------------- |\n| `baseUrl`        | `string`                    | `\"\"`              | Prepended to all paths.                                        |\n| `headers`        | `Record<string, string>`    | `{}`              | Default headers sent with every request.                       |\n| `getToken`       | `() => string\\|null`        | —                 | Returns the current auth token. Injected as `Bearer`.          |\n| `onTokenExpired` | `() => Promise<string>`     | —                 | Called on 401. Should refresh and return the new token.        |\n| `retries`        | `number`                    | `0`               | How many times to retry on server errors or network failures.  |\n| `retryDelay`     | `number` (ms)               | `300`             | Base delay between retries. Doubles on each attempt (backoff). |\n| `retryOn`        | `number[]`                  | `[500,502,503,504]` | HTTP statuses that trigger a retry.                          |\n| `timeout`        | `number` (ms)               | —                 | Throws `TimeoutError` if the request exceeds this.             |\n| `onRequest`      | `(config) => config`        | —                 | Interceptor run before each request.                           |\n| `onResponse`     | `({ status, data, headers }) => any` | —      | Interceptor run after each successful parse.                   |\n| `onError`        | `(err: FetchError) => void` | —                 | Called for every error before it's thrown.                     |\n\n---\n\n## Methods\n\n```js\napi.get(path, options?)                 // GET\napi.post(path, body?, options?)         // POST\napi.put(path, body?, options?)          // PUT\napi.patch(path, body?, options?)        // PATCH\napi.delete(path, options?)              // DELETE\napi.request(method, path, options?)     // custom method\n```\n\n### Options per request\n\n```js\napi.get(\"/users\", {\n  query:   { page: 1, limit: 20 },        // appended as ?page=1&limit=20\n  headers: { \"X-Request-ID\": \"abc\" },     // merged with default headers\n  signal:  controller.signal,             // AbortController support\n});\n```\n\n---\n\n## Token Refresh (401 flow)\n\nWhen a request gets a 401:\n1. `onTokenExpired()` is called once.\n2. The request is retried with the new token.\n3. If the retry also gets 401, a `FetchError(401)` is thrown — no infinite loop.\n\n```js\nconst api = createFetchKit({\n  baseUrl: \"https://api.example.com\",\n  getToken: () => store.getState().token,\n  onTokenExpired: async () => {\n    const res = await fetch(\"/auth/refresh\", { method: \"POST\" });\n    const { token } = await res.json();\n    store.dispatch(setToken(token));\n    return token;\n  },\n});\n```\n\n---\n\n## Retry with Backoff\n\n```js\nconst api = createFetchKit({\n  baseUrl: \"https://api.example.com\",\n  retries: 3,          // up to 3 retries\n  retryDelay: 500,     // 500ms → 1000ms → 2000ms (exponential)\n  retryOn: [500, 502, 503, 504],\n});\n```\n\nRetries also trigger on network errors (e.g. DNS failure, connection refused). 4xx errors are **not** retried (except 401 with `onTokenExpired`).\n\n---\n\n## Timeout + Abort\n\n```js\n// Timeout\nconst api = createFetchKit({ baseUrl: \"...\", timeout: 5000 });\n\n// Per-request abort\nconst controller = new AbortController();\nsetTimeout(() => controller.abort(), 3000);\nconst data = await api.get(\"/stream\", { signal: controller.signal });\n```\n\n---\n\n## Interceptors\n\n```js\nconst api = createFetchKit({\n  baseUrl: \"https://api.example.com\",\n\n  // Modify the request before it's sent\n  onRequest: (config) => ({\n    ...config,\n    headers: { ...config.headers, \"X-Request-ID\": crypto.randomUUID() },\n  }),\n\n  // Transform the response data\n  onResponse: ({ data }) => data.result ?? data,\n\n  // Log every error\n  onError: (err) => logger.error({ status: err.status, url: err.url }),\n});\n```\n\n---\n\n## Error Handling\n\n```js\nimport { FetchError, TimeoutError } from \"@bonhomie/fetch-kit\";\n\ntry {\n  const data = await api.get(\"/users\");\n} catch (err) {\n  if (err instanceof TimeoutError) {\n    console.log(\"Request timed out\");\n  } else if (err instanceof FetchError) {\n    console.log(err.status);       // 404, 500, null (network)\n    console.log(err.body);         // parsed response body\n    console.log(err.url);          // URL that failed\n    console.log(err.method);       // \"GET\", \"POST\", etc.\n\n    if (err.isUnauthorized())  { /* 401 */ }\n    if (err.isForbidden())     { /* 403 */ }\n    if (err.isNotFound())      { /* 404 */ }\n    if (err.isClientError())   { /* 4xx */ }\n    if (err.isServerError())   { /* 5xx */ }\n    if (err.isNetworkError())  { /* no response */ }\n  }\n}\n```\n\n---\n\n## Security Notes\n\n- **Header injection** — all header values are stripped of `\\r`, `\\n`, and null bytes before the request is sent. An attacker who can influence header values cannot inject additional HTTP headers.\n- **Auth tokens** — tokens are never included in error messages.\n- **Non-serializable bodies** — `JSON.stringify` failures are caught and thrown as `FetchError` instead of crashing with an unhandled exception.\n\n---\n\n## 📄 License\n\nMIT — **Bonhomie**\n","readmeFilename":"README.md","_rev":"1-69f29a8a2b57c1e96555a3b857979e8d"}