{"_id":"@darna-digital/composable-fetcher","_rev":"5-c2394ed98bf3dd17cf25e62a62e7f890","name":"@darna-digital/composable-fetcher","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@darna-digital/composable-fetcher","version":"0.1.0","keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"author":{"name":"Darna Digital"},"license":"MIT","_id":"@darna-digital/composable-fetcher@0.1.0","maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"homepage":"https://github.com/darna-digital/composable-fetcher#readme","bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"dist":{"shasum":"645b22ee5eec57a5964cedf53f52da25227d40f6","tarball":"https://registry.npmjs.org/@darna-digital/composable-fetcher/-/composable-fetcher-0.1.0.tgz","fileCount":17,"integrity":"sha512-1yIg7AyXf9bWJhTid0b6KnfYfKJt0h/0vj0MaxrkFOjHf1uAFfnMvFLB75KgvwRFpJRNepRhtbdMZ2OiWHAivw==","signatures":[{"sig":"MEUCIA/9bUOD7FVm+iRQjnYfumpKEEDxjUbPgQJHWXBrxm8EAiEAnTiyNmNlSJnXBPA2XsP4cYjp9bJtzOVb1FirB/P5Ghc=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":94234},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}}},"gitHead":"60d570d5f8db389178ca7e69f90c25b1d826ae3e","scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsup","release":"npm run lint && npm run test && npm run build && npm publish --access public","test:watch":"vitest","release:dry":"npm run lint && npm run test && npm run build && npm publish --access public --dry-run","release:major":"npm version major && npm run release","release:minor":"npm version minor && npm run release","release:patch":"npm version patch && npm run release","prepublishOnly":"npm run lint && npm run test && npm run build"},"_npmUser":{"name":"railarutenis","email":"rutenis@darnadigital.com"},"repository":{"url":"git+https://github.com/darna-digital/composable-fetcher.git","type":"git"},"_npmVersion":"11.4.0","description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","vitest":"^3.0.0","typescript":"^5.7.0"},"_npmOperationalInternal":{"tmp":"tmp/composable-fetcher_0.1.0_1771698303373_0.8697545654225118","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@darna-digital/composable-fetcher","version":"0.1.1","keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"author":{"name":"Darna Digital"},"license":"MIT","_id":"@darna-digital/composable-fetcher@0.1.1","maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"homepage":"https://github.com/darna-digital/composable-fetcher#readme","bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"dist":{"shasum":"4a95146187e7fd7dac8ccf9e06524b2a99286ce5","tarball":"https://registry.npmjs.org/@darna-digital/composable-fetcher/-/composable-fetcher-0.1.1.tgz","fileCount":17,"integrity":"sha512-BeadTTa65tt2cBnTINVeSqUdNLcJ6zTs8corim1nOsczgjJcYGP1Zu8W/7cTcBMfwN5yih2YJnudxskfwIbNeQ==","signatures":[{"sig":"MEYCIQCSvHqO3ocSSOfBa6ENyAs5ofJIn2tsaRQpNJ1h2UGMqAIhAMqlDwfXRabMNlJBieDy09Qo6mbiszQBQ2uGjTOCbjGd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":92642},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}}},"gitHead":"2ea18527757ff859f36540eed88d33cd7858b3d2","scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsup","release":"npm run lint && npm run test && npm run build && npm publish --access public","test:watch":"vitest","release:dry":"npm run lint && npm run test && npm run build && npm publish --access public --dry-run","release:major":"npm version major && npm run release","release:minor":"npm version minor && npm run release","release:patch":"npm version patch && npm run release","prepublishOnly":"npm run lint && npm run test && npm run build"},"_npmUser":{"name":"railarutenis","email":"rutenis@darnadigital.com"},"repository":{"url":"git+https://github.com/darna-digital/composable-fetcher.git","type":"git"},"_npmVersion":"11.4.0","description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","vitest":"^3.0.0","typescript":"^5.7.0"},"_npmOperationalInternal":{"tmp":"tmp/composable-fetcher_0.1.1_1771700350172_0.07964938003981481","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@darna-digital/composable-fetcher","version":"0.1.2","keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"author":{"name":"Darna Digital"},"license":"MIT","_id":"@darna-digital/composable-fetcher@0.1.2","maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"homepage":"https://github.com/darna-digital/composable-fetcher#readme","bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"dist":{"shasum":"3b0e600e5595407d03cf366a50656d87ceae1521","tarball":"https://registry.npmjs.org/@darna-digital/composable-fetcher/-/composable-fetcher-0.1.2.tgz","fileCount":16,"integrity":"sha512-EogvaX8KFDyVnLUFMDKe+X7jz821HttNunNIcMOcCE/fDfWcw+W6tWIqGuw6bDuaccKZvMxGRTfr18ToTZBASA==","signatures":[{"sig":"MEQCIDv/h1Vjkk3kWp1ajgYsaoeCIjTYikzcGQPQ/KA7sAivAiBsBO6JMfdSjzAvtHjQyeRQpdu4H/jqkElypSs1Ab/qfg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":106214},"main":"./dist/index.cjs","type":"module","_from":"file:darna-digital-composable-fetcher-0.1.2.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}}},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsup","release":"pnpm run lint && pnpm run test && pnpm run build && pnpm publish --access public","test:watch":"vitest","release:dry":"pnpm run lint && pnpm run test && pnpm run build && pnpm publish --access public --dry-run","release:major":"pnpm version major && pnpm run release","release:minor":"pnpm version minor && pnpm run release","release:patch":"pnpm version patch && pnpm run release"},"_npmUser":{"name":"railarutenis","email":"rutenis@darnadigital.com"},"_resolved":"/private/var/folders/41/4cdtl6yx0951rzqp7kd1gl9r0000gn/T/9be617e18d3f6f1801e9a56b46817671/darna-digital-composable-fetcher-0.1.2.tgz","_integrity":"sha512-EogvaX8KFDyVnLUFMDKe+X7jz821HttNunNIcMOcCE/fDfWcw+W6tWIqGuw6bDuaccKZvMxGRTfr18ToTZBASA==","repository":{"url":"git+https://github.com/darna-digital/composable-fetcher.git","type":"git"},"_npmVersion":"10.8.2","description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.0.18","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/composable-fetcher_0.1.2_1775411613042_0.10122443800002379","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"@darna-digital/composable-fetcher","version":"0.1.3","keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"author":{"name":"Darna Digital"},"license":"MIT","_id":"@darna-digital/composable-fetcher@0.1.3","maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"homepage":"https://github.com/darna-digital/composable-fetcher#readme","bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"dist":{"shasum":"8c3c9f42008ebe1ec9f6c384e7aa408d229cef24","tarball":"https://registry.npmjs.org/@darna-digital/composable-fetcher/-/composable-fetcher-0.1.3.tgz","fileCount":16,"integrity":"sha512-SJpQRJuOwPjSx65gBPOCFFW6FHy+oZjQeJapktIm5v8tYg+JRU0CN6PmvdSLzQ3UxLL3g1qC7Oqj0SAmI73YOg==","signatures":[{"sig":"MEUCID15IOV2Ca5iQtHFx8kCfK9+z4IcK7jhrxsyBasdTYjiAiEAnzwx2rD6SOPOzyqHS+JvTqY83zFVy8CiKeLaG/42vUU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":106214},"main":"./dist/index.cjs","type":"module","_from":"file:darna-digital-composable-fetcher-0.1.3.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}}},"scripts":{"lint":"tsc --noEmit","test":"vitest run","build":"tsup","release":"pnpm run lint && pnpm run test && pnpm run build && pnpm publish --access public","test:watch":"vitest","release:dry":"pnpm run lint && pnpm run test && pnpm run build && pnpm publish --access public --dry-run","release:major":"pnpm version major && pnpm run release","release:minor":"pnpm version minor && pnpm run release","release:patch":"pnpm version patch && pnpm run release"},"_npmUser":{"name":"railarutenis","email":"rutenis@darnadigital.com"},"_resolved":"/private/var/folders/41/4cdtl6yx0951rzqp7kd1gl9r0000gn/T/964370dd42c4a08ccab4d82e23609513/darna-digital-composable-fetcher-0.1.3.tgz","_integrity":"sha512-SJpQRJuOwPjSx65gBPOCFFW6FHy+oZjQeJapktIm5v8tYg+JRU0CN6PmvdSLzQ3UxLL3g1qC7Oqj0SAmI73YOg==","repository":{"url":"git+https://github.com/darna-digital/composable-fetcher.git","type":"git"},"_npmVersion":"10.8.2","description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","vitest":"^4.0.18","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/composable-fetcher_0.1.3_1775411984739_0.8828633655941216","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@darna-digital/composable-fetcher","version":"0.1.4","description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./testing":{"import":{"types":"./dist/testing.d.ts","default":"./dist/testing.js"},"require":{"types":"./dist/testing.d.cts","default":"./dist/testing.cjs"}}},"keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"author":{"name":"Darna Digital"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/darna-digital/composable-fetcher.git"},"homepage":"https://github.com/darna-digital/composable-fetcher#readme","bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"devDependencies":{"tsup":"^8.5.1","typescript":"^5.9.3","vitest":"^4.0.18"},"engines":{"node":">=18"},"sideEffects":false,"scripts":{"build":"tsup","test":"vitest run","test:watch":"vitest","lint":"tsc --noEmit","release":"pnpm run lint && pnpm run test && pnpm run build && cp ../../README.md . && pnpm publish --access public && rm README.md","release:dry":"pnpm run lint && pnpm run test && pnpm run build && cp ../../README.md . && pnpm publish --access public --dry-run && rm README.md","release:patch":"pnpm version patch && pnpm run release","release:minor":"pnpm version minor && pnpm run release","release:major":"pnpm version major && pnpm run release"},"_id":"@darna-digital/composable-fetcher@0.1.4","_integrity":"sha512-nFjhFrmfdxczjE4MgV4DpvcGaL+tTW2r8qZoq9Ocvbsjlak1jQaPJ3emiX04ujlInHID55DemAHbiaDKPboVkw==","_resolved":"/private/var/folders/41/4cdtl6yx0951rzqp7kd1gl9r0000gn/T/1ddd159b976537ccbc3411e47952aac4/darna-digital-composable-fetcher-0.1.4.tgz","_from":"file:darna-digital-composable-fetcher-0.1.4.tgz","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-nFjhFrmfdxczjE4MgV4DpvcGaL+tTW2r8qZoq9Ocvbsjlak1jQaPJ3emiX04ujlInHID55DemAHbiaDKPboVkw==","shasum":"435a99c076d88420790cfb87fea5e673ac142501","tarball":"https://registry.npmjs.org/@darna-digital/composable-fetcher/-/composable-fetcher-0.1.4.tgz","fileCount":17,"unpackedSize":115976,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDc9PdwiZ4VzFhu87Nn+SKJY01QvbhVz9tiaPP22Btf5wIgRsCosHBc0w2xgdWzyQXvg5zzINZVIJWqswfazcJHsg4="}]},"_npmUser":{"name":"railarutenis","email":"rutenis@darnadigital.com"},"directories":{},"maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/composable-fetcher_0.1.4_1775412115325_0.26703565612991076"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-21T18:25:03.309Z","modified":"2026-04-05T18:01:55.613Z","0.1.0":"2026-02-21T18:25:03.544Z","0.1.1":"2026-02-21T18:59:10.317Z","0.1.2":"2026-04-05T17:53:33.167Z","0.1.3":"2026-04-05T17:59:44.886Z","0.1.4":"2026-04-05T18:01:55.483Z"},"bugs":{"url":"https://github.com/darna-digital/composable-fetcher/issues"},"author":{"name":"Darna Digital"},"license":"MIT","homepage":"https://github.com/darna-digital/composable-fetcher#readme","keywords":["fetch","http","client","composable","builder","standard-schema","validation","zod","valibot","arktype","retry","observability","typescript"],"repository":{"type":"git","url":"git+https://github.com/darna-digital/composable-fetcher.git"},"description":"Builder-based HTTP fetcher with Standard Schema validation, typed error decoding, composable error handling with retry, and observability.","maintainers":[{"name":"railarutenis","email":"rutenis@darnadigital.com"}],"readme":"<p align=\"center\">\n  <img src=\"art/composable-fetcher-logo.png\" alt=\"Composable Fetcher\" />\n</p>\n\nBuilder-based HTTP fetcher with Standard Schema validation, typed error decoding, retry, and observability.\n\n- Zero dependencies — native `fetch`\n- Standard Schema (Effect Schema, Zod, Valibot, ArkType, etc.)\n- Full type inference from schema to response\n- Input validation — catch bad data before it hits the server\n- Inline error handling with retry\n- Observability via span events\n\n## Install\n\n```bash\npnpm add @darna-digital/composable-fetcher\n```\n\n## Quick start\n\n```ts\nimport { composableFetcher } from '@darna-digital/composable-fetcher';\nimport { z } from 'zod';\n\nconst UsersSchema = z.array(z.object({ id: z.string(), name: z.string() }));\n\n// GET with response validation\nconst users = await composableFetcher\n  .url('/api/users')\n  .schema(UsersSchema)\n  .run('GET');\n\n// POST with input validation\nconst CreateUserSchema = z.object({ email: z.string().email(), name: z.string() });\n\nawait composableFetcher\n  .url('/api/users')\n  .input(CreateUserSchema)\n  .body({ email: 'foo@bar.com', name: 'Alice' })\n  .run('POST');\n```\n\n## Builder API\n\n```ts\ncomposableFetcher\n  .url(url)                          // target URL\n  .schema(schema)                    // response validation (Standard Schema)\n  .input(schema)                     // input validation for the request body\n  .errorSchema(schema, extractor?)   // backend error body validation\n  .formatError(formatter)            // format thrown Error.message from FetchError\n  .name('getUsers')                  // span name, defaults to \"METHOD /url\"\n  .fallback('Load failed')           // fallback error message\n  .headers({ 'X-Key': '1' })        // per-request headers\n  .body({ key: 'value' })           // body for mutations\n  .catch(handler)                    // error handling with retry\n  .run('GET');                       // executes the request, returns Promise<T>\n```\n\n## Custom instance\n\n```ts\nimport { createComposableFetcher } from '@darna-digital/composable-fetcher';\n\nconst api = createComposableFetcher({\n  fetchFn: customFetch,\n  headers: { 'X-App': '1.0' },\n  credentials: 'include',\n  cache: 'no-store',\n  onSpan: (event) => log(event),\n  catch: globalCatchHandler,\n  errorSchema: BackendErrorSchema,\n  errorMessage: (data) => extractMessage(data),\n});\n\n// Derive from existing\nconst adminApi = api.configure({ headers: { 'X-Role': 'admin' } });\n```\n\n| Option         | Type                         | Default            |\n| -------------- | ---------------------------- | ------------------ |\n| `fetchFn`      | `typeof fetch`               | `globalThis.fetch` |\n| `headers`      | `Record<string, string>`     | `{}`               |\n| `credentials`  | `RequestCredentials`         | `'include'`        |\n| `cache`        | `RequestCache`               | `'no-store'`       |\n| `onSpan`       | `(event: SpanEvent) => void` | —                  |\n| `catch`        | `CatchHandler`               | —                  |\n| `errorSchema`  | `StandardSchema`             | —                  |\n| `errorMessage` | `(data: unknown) => string`  | —                  |\n| `errorFormatter` | `(error: FetchError) => string` | —             |\n\n## Error handling\n\n`.catch()` handles errors inline. The handler receives `{ error, retry }`.\n\n```ts\ntype CatchHandler<E> = (params: {\n  error: FetchError<E>;\n  retry: (options?: { headers?: Record<string, string> }) => Promise<unknown>;\n}) => Promise<unknown> | void;\n```\n\n- Return nothing — error is swallowed, promise resolves to `undefined`\n- Return `retry()` — request is retried transparently (once per request)\n- No `.catch()` — errors throw as normal\n\nFor UI flows (e.g. React Query `onError`), prefer `errorFormatter` and use `error.message` directly:\n\n```ts\nconst api = createComposableFetcher({\n  errorFormatter: (error) => {\n    if (error.type === 'input' || error.type === 'parse') {\n      return error.issues.join(', ');\n    }\n    if (error.type === 'http' && error.data?.issues?.length) {\n      return error.data.issues.join(', ');\n    }\n    return error.message;\n  },\n});\n\nonError: (error) => {\n  setResult(error.message);\n};\n```\n\nHelper exports (`getFetchError`, `isComposableFetcherError`, `toErrorMessage`) remain available for ad-hoc error inspection.\n\nSet at config level (global) or builder level (per-request). Builder overrides config.\n\n### Handling validation errors\n\nWhen `errorSchema` is set, `error.data` is fully typed — no casting.\n\n```ts\nawait api\n  .url('/api/users')\n  .body({ email, name })\n  .errorSchema(ViolationsSchema)\n  .catch(({ error }) => {\n    if (error.type === 'http' && error.data) {\n      for (const v of error.data.violations) {\n        form.setError(v.propertyPath, { message: v.message });\n      }\n    }\n  })\n  .run('POST');\n```\n\n### JWT refresh\n\n```ts\nconst api = createComposableFetcher({\n  headers: { Authorization: `Bearer ${getAccessToken()}` },\n  catch: async ({ error, retry }) => {\n    if (error.type !== 'http' || error.status !== 401) return;\n    const { accessToken } = await refreshAccessToken();\n    setAccessToken(accessToken);\n    return retry({ headers: { Authorization: `Bearer ${accessToken}` } });\n  },\n});\n```\n\n## Error types\n\nDiscriminated union — narrow on `type`:\n\n```ts\ntype NetworkError = { type: 'network'; message: string };\ntype HttpError<D> = { type: 'http'; status: number; statusText: string; message: string; data?: D };\ntype ParseError   = { type: 'parse'; message: string; issues: string[] };\ntype InputError   = { type: 'input'; message: string; issues: string[] };\ntype FetchError<D> = NetworkError | HttpError<D> | ParseError | InputError;\n```\n\n`D` defaults to `unknown`. When `errorSchema` is set, `D` is inferred from the schema.\n\n## Backend error schemas\n\nDifferent backends return different error shapes. `errorSchema` decodes them with type safety.\n\n```ts\n// Symfony\nconst api = createComposableFetcher({\n  errorSchema: z.object({\n    violations: z.array(z.object({\n      propertyPath: z.string(),\n      message: z.string(),\n    })),\n  }),\n  errorMessage: (data) => data.violations.map(v => v.message).join(', '),\n});\n\n// Laravel\nconst api = createComposableFetcher({\n  errorSchema: z.object({\n    errors: z.record(z.array(z.string())),\n  }),\n  errorMessage: (data) => Object.values(data.errors).flat().join(', '),\n});\n```\n\nPer-request override:\n\n```ts\nawait api\n  .url('/api/external')\n  .errorSchema(ExternalErrorSchema, (data) => data.detail)\n  .run('POST');\n```\n\n## Input validation\n\n`.input(schema)` validates the request body before it leaves the client.\n\n```ts\nconst CreateUserSchema = z.object({\n  email: z.string().email(),\n  name: z.string().min(1),\n});\n\nawait api\n  .url('/api/users')\n  .input(CreateUserSchema)\n  .body({ email: 'alice@test.com', name: 'Alice' })\n  .schema(UserSchema)\n  .run('POST');\n```\n\n`.body()` is type-checked against the input schema — pass the wrong shape and TS catches it. At runtime, bad data produces an `InputError` and the request never fires:\n\n```ts\nawait api\n  .url('/api/users')\n  .input(CreateUserSchema)\n  .body({ email: 'alice@test.com', name: 'Alice' })\n  .catch(({ error }) => {\n    if (error.type === 'input') {\n      console.log(error.issues); // [\"email: Invalid email\"]\n    }\n  })\n  .run('POST');\n```\n\nWithout `.input()`, `.body()` accepts anything — same as before.\n\n## Headers\n\nMerged in order (later wins):\n\n1. Built-in (`Accept: application/json`, `Content-Type` for mutations)\n2. Instance `headers`\n3. Builder `.headers()`\n4. Retry `headers`\n\n## Observability\n\nEvery request emits a `SpanEvent`:\n\n```ts\ntype SpanEvent = {\n  name: string;\n  op: 'query' | 'mutate';\n  url: string;\n  method: string;\n  status?: number;\n  durationMs: number;\n  ok: boolean;\n  error?: FetchError;\n};\n```\n\n```ts\ncreateComposableFetcher({\n  onSpan: (event) => {\n    Sentry.startSpan({ name: event.name, op: `http.${event.op}` }, (span) => {\n      span.setAttribute('http.method', event.method);\n      span.setAttribute('http.url', event.url);\n      span.setAttribute('http.response_time_ms', event.durationMs);\n      if (event.status) span.setAttribute('http.status_code', event.status);\n      if (!event.ok) span.setStatus({ code: 2, message: event.error?.message });\n    });\n  },\n});\n```\n\n## Testing\n\nTest utilities from a separate entry point:\n\n```ts\nimport {\n  createComposableFetcherDependenciesMock,\n  mockFetchResponse,\n  mockFetchNetworkError,\n  createMockSchema,\n  createFailingMockSchema,\n} from '@darna-digital/composable-fetcher/testing';\nimport { createComposableFetcherFunctions } from '@darna-digital/composable-fetcher';\n\nconst fetchMock = vi.fn();\nconst deps = createComposableFetcherDependenciesMock({ fetch: fetchMock });\nconst fns = createComposableFetcherFunctions(deps);\n\nmockFetchResponse(fetchMock, { status: 200, body: { users: [] } });\n\nconst result = await fns.execute({\n  url: '/api/users',\n  method: 'GET',\n  op: 'query',\n  name: 'getUsers',\n  fallback: 'failed',\n  headers: {},\n  schema: createMockSchema({ users: [] }),\n});\n\nexpect(result).toEqual({ users: [] });\n```\n\n| Helper                                     | Description                        |\n| ------------------------------------------ | ---------------------------------- |\n| `createComposableFetcherDependenciesMock()` | Full dependencies mock             |\n| `mockFetchResponse(fetchMock, config)`      | Queue a response                   |\n| `mockFetchNetworkError(fetchMock)`          | Queue a network failure            |\n| `createMockSchema<T>(output)`              | Schema that always succeeds        |\n| `createFailingMockSchema(message)`         | Schema that always fails           |\n\n## License\n\nMIT\n","readmeFilename":"README.md"}