{"_id":"@alexovn/okapi","name":"@alexovn/okapi","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@alexovn/okapi","type":"module","version":"1.0.0","private":false,"description":"A library for handling API and HTTP errors with ease.","author":{"name":"Nikita Aleksov","url":"https://github.com/alexovn/"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/alexovn/okapi.git"},"bugs":{"url":"https://github.com/alexovn/okapi/issues"},"homepage":"https://github.com/alexovn/okapi#readme","keywords":["okapi","api","http","errors","fetch","ofetch","axios"],"sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","default":"./dist/index.js"},"./fetch":{"types":"./dist/fetch.d.ts","import":"./dist/fetch.js","default":"./dist/fetch.js"},"./axios":{"types":"./dist/axios.d.ts","import":"./dist/axios.js","default":"./dist/axios.js"},"./ofetch":{"types":"./dist/ofetch.d.ts","import":"./dist/ofetch.js","default":"./dist/ofetch.js"}},"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","scripts":{"build":"tsc && vite build","test":"vitest run","typecheck":"tsc --noEmit","prepack":"node ../../scripts/copy-publish-files.mjs && pnpm build","prepublishOnly":"pnpm --workspace-root lint packages/lib && pnpm test"},"peerDependencies":{"axios":"^1.18.1","ofetch":"^1.5.1"},"peerDependenciesMeta":{"axios":{"optional":true},"ofetch":{"optional":true}},"devDependencies":{"@microsoft/api-extractor":"^7.52.0","axios":"^1.18.1","ofetch":"^1.5.1","typescript":"~6.0.2","unplugin-dts":"^1.0.3","vite":"^8.0.12","vitest":"^4.1.10"},"engines":{"node":">=22","pnpm":">=11"},"gitHead":"1450c7a74d734ec4c6cd11f8557bbc3120dfef1f","_id":"@alexovn/okapi@1.0.0","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-50xsZDsgkovEwn31HN718axn3lC/0my82P7NSMFi+F0Zp0dexhEtjW+01TOAlZzLYvOO+2NCJ63Bh335fc7bdg==","shasum":"a265096c1000c7c620ceac919462f1f7bff4a58f","tarball":"https://registry.npmjs.org/@alexovn/okapi/-/okapi-1.0.0.tgz","fileCount":12,"unpackedSize":54668,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIBbYoaV6E6QLQiNYh78f0Qa2fsgRRNiYhseM4l8OeI9LAiBbzueWfV5cxsQVOnNt1uKWVH/AQ3kIuqXRqqV2etkzlw=="}]},"_npmUser":{"name":"alexovn","email":"sunrisebeforethestorm@gmail.com"},"directories":{},"maintainers":[{"name":"alexovn","email":"sunrisebeforethestorm@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/okapi_1.0.0_1785333773295_0.5063379295116679"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-29T14:02:53.067Z","1.0.0":"2026-07-29T14:02:53.442Z","modified":"2026-07-29T14:02:53.689Z"},"maintainers":[{"name":"alexovn","email":"sunrisebeforethestorm@gmail.com"}],"description":"A library for handling API and HTTP errors with ease.","homepage":"https://github.com/alexovn/okapi#readme","keywords":["okapi","api","http","errors","fetch","ofetch","axios"],"repository":{"type":"git","url":"git+https://github.com/alexovn/okapi.git"},"author":{"name":"Nikita Aleksov","url":"https://github.com/alexovn/"},"bugs":{"url":"https://github.com/alexovn/okapi/issues"},"license":"MIT","readme":"![Okapi banner](./.github/assets/okapi-banner.jpg)\n\n# Okapi\n\nA library for handling API and HTTP errors with ease.\n\n## Features\n\n- Framework agnostic\n- Fully typed\n- i18n support\n- Built-in adapters for popular fetching libraries (axios, ofetch etc.)\n- Customizable. Set up your own error kinds and validation-error formats\n\n## Installation\n\n```shell\nnpm install @alexovn/okapi\n```\n\n> ⚠️ Fetching data libraries like `Axios` or `ofetch` should be installed separately.\n\n## Quick Start\n\nUse an adapter mapper to turn a library-specific error into a consistent object for your UI:\n\n```ts\nimport axios from 'axios'\nimport { createAxiosErrorMapper } from '@alexovn/okapi/axios'\nimport { notify } from './notificationService' // your notification service\n\nconst mapAxiosError = createAxiosErrorMapper({\n  i18n: {\n    titles: {\n      'not-found': 'Not found',\n      network: 'Connection problem',\n    },\n    statusTitles: {\n      503: 'Temporarily unavailable',\n    },\n    messages: {\n      'not-found': 'The requested resource was not found.',\n      network: 'Check your internet connection and try again.',\n    },\n    statusMessages: {\n      503: 'The service is temporarily unavailable. Please try again later.',\n    },\n  },\n})\n\nasync function saveProfile() {\n  try {\n    const response = await axios.post('/api/profile', {\n      email: 'invalid',\n    })\n    return response.data\n  } catch (error) {\n    const mappedError = mapAxiosError(error)\n\n    notify({\n      title: mappedError.title,\n      message: mappedError.message\n    })\n  }\n}\n```\n\n## Core API\n\n### `createApiErrorFromResponse`\n\nCreates an `OkapiError` from an HTTP status and a parsed response body:\n\n```ts\nimport { createApiErrorFromResponse } from '@alexovn/okapi'\n\nconst error = createApiErrorFromResponse({\n  status: 422,\n  body: {\n    message: 'Invalid email',\n    errors: { email: ['Required'] },\n  },\n})\n\n// Example\n\n// kind: 'validation'\n// source: 'api'\n// statusCode: 422\n// message: 'Passed data has issues'\n// rawMessage: 'Invalid email',\n// validationErrors: { email: ['Required'] }\n```\n\nThe API-provided message remains available as `rawMessage`. The normalized `message` uses Okapi's\nbuilt-in or configured message for the resolved error kind.\n\n### `normalizeOkapiError`\n\nConverts an unknown thrown value to an `OkapiError`. Existing `OkapiError` instances are preserved:\n\n```ts\nimport { normalizeOkapiError } from '@alexovn/okapi'\n\ntry {\n  await save()\n} catch (error) {\n  throw normalizeOkapiError(error)\n}\n\n// Example\n\n// type: 'unexpected'\n// title: 'Something went wrong'\n// message: 'Unexpected error occurred'\n// details: OkapiError\n```\n\nGeneric `Error` values become `unexpected` errors. Fetch adapters should be used when a\n`TypeError` needs to be recognized as a network failure.\n\n### `mapOkapiError`\n\nConverts an unknown error into a presentation-friendly `MappedOkapiError`:\n\n```ts\nimport { mapOkapiError } from '@alexovn/okapi'\n\nconst mappedError = mapOkapiError(new Error('Failed'))\n\n// Example\n\n// type: 'unexpected'\n// title: 'Something went wrong'\n// message: 'Unexpected error occurred'\n// details: OkapiError\n```\n\n### `OkapiError`\n\n`OkapiError` extends the native `Error` class and provides:\n\n- `kind`: the specific error category\n- `source`: where the error originated\n- `statusCode` and `statusText`: HTTP response information, when available\n- `validationErrors`: parsed validation details, when available\n- `raw`: the original API or HTTP response body\n- `rawMessage`: the original message from a recognized API response\n- `cause`: the original thrown value, when available\n- `isNetworkError`: flag that checks if an error is a network error\n- `isValidationError`: flag that checks if an error is a validation error\n\nIt can also be constructed directly or through its static helpers:\n\n```ts\nimport { OkapiError } from '@alexovn/okapi'\n\nconst error = new OkapiError({\n  kind: 'business',\n  message: 'The operation could not be completed.',\n})\n```\n\nAvailable static helpers are:\n\n- `OkapiError.getApiResponseError` handles api error.\n- `OkapiError.getHttpResponseError` handles http error.\n- `OkapiError.getNetworkError` handles network error.\n- `OkapiError.getUnexpectedError` handles unexpected error.\n\n## Error Taxonomy\n\n`kind` describes the specific failure, while `type` groups kinds into broader categories suitable\nfor application behavior.\n\n| Kind | Mapped type | Typical cause |\n| --- | --- | --- |\n| `network` | `network` | Transport or connection failure |\n| `abort` | `network` | Aborted request |\n| `unauthorized` | `auth` | HTTP 401 |\n| `forbidden` | `business` | HTTP 403 |\n| `not-found` | `business` | HTTP 404 |\n| `validation` | `validation` | HTTP 422 or recognized validation errors |\n| `conflict` | `business` | HTTP 409 |\n| `rate-limited` | `business` | HTTP 429 |\n| `business` | `business` | Other recognized API or HTTP failure |\n| `server` | `server` | HTTP 5xx |\n| `unexpected` | `unexpected` | Unrecognized thrown value |\n\nAn error's `source` is one of `api`, `http`, `network`, `unexpected`, or `custom`.\n\n## Fetch Adapters\n\nIf you want to get started quickly, take a look at the built-in fetch adapters. Each adapter is a\nsmall wrapper around the library functions and provides everything you need to handle errors the\nright way.\n\n### Native Fetch\n\nImport native Fetch helpers from `@alexovn/okapi/fetch`:\n\n- `getFetchResponseError` handles unsuccessful HTTP responses.\n- `getFetchError` handles response-like objects, network errors, and other thrown values.\n- `createFetchResponseErrorMapper` creates a mapper for unsuccessful HTTP responses.\n- `createFetchErrorMapper` creates a mapper for network and other thrown errors.\n\nThe native Fetch adapter does not read the response body for you. Parse it once and pass it as the\nsecond argument:\n\n```ts\nimport {\n  createFetchErrorMapper,\n  createFetchResponseErrorMapper,\n} from '@alexovn/okapi/fetch'\n\nconst mapFetchError = createFetchErrorMapper()\nconst mapFetchResponseError = createFetchResponseErrorMapper()\n\nasync function fetchData<T>(url: string, options?: RequestInit): Promise<T> {\n  let response: Response\n\n  try {\n    response = await fetch(url, options)\n  } catch (error) {\n    throw mapFetchError(error)\n  }\n\n  if (!response.ok) {\n    let body: unknown\n\n    try {\n      body = await response.json()\n    } catch {\n      // The response may have an empty or non-JSON body.\n    }\n\n    throw mapFetchResponseError(response, body)\n  }\n\n  return (await response.json()) as T\n}\n```\n\nThis example throws a mapped object to its caller. If your application expects native `Error`\ninstances, use `getFetchError` and `getFetchResponseError` instead, then map the error at the UI\nboundary.\n\n### Axios\n\nImport Axios helpers from `@alexovn/okapi/axios`:\n\n- `getAxiosError` returns an `OkapiError`.\n- `createAxiosErrorMapper` creates a reusable `MappedOkapiError` mapper.\n\nAxios exposes the parsed error response body to the adapter automatically:\n\n```ts\nimport axios from 'axios'\nimport { createAxiosErrorMapper } from '@alexovn/okapi/axios'\n\nconst mapAxiosError = createAxiosErrorMapper()\n\nexport async function axiosGet<T>(url: string): Promise<T> {\n  try {\n    const response = await axios.get<T>(url)\n    return response.data\n  } catch (error) {\n    throw mapAxiosError(error)\n  }\n}\n```\n\n### ofetch\n\nImport ofetch helpers from `@alexovn/okapi/ofetch`:\n\n- `getOfetchError` returns an `OkapiError`.\n- `createOfetchErrorMapper` creates a reusable `MappedOkapiError` mapper.\n\n```ts\nimport { ofetch } from 'ofetch'\nimport { createOfetchErrorMapper } from '@alexovn/okapi/ofetch'\n\nconst mapOfetchError = createOfetchErrorMapper()\n\nexport async function ofetchGet<T>(url: string): Promise<T> {\n  try {\n    return await ofetch<T>(url)\n  } catch (error) {\n    throw mapOfetchError(error)\n  }\n}\n```\n\n## Translations\n\nOkapi includes English titles and messages by default. They are resolved when an error passes\nthrough `mapOkapiError` or an adapter mapper.\n\nUse kind-based values for general translations and status-based values for more specific HTTP\nresponses:\n\n```ts\nimport type { OkapiErrorAdapterOptions } from '@alexovn/okapi'\n\nconst options: OkapiErrorAdapterOptions = {\n  i18n: {\n    titles: {\n      'not-found': 'Not found',\n      network: 'Connection problem',\n    },\n    statusTitles: {\n      503: 'Temporarily unavailable',\n    },\n    messages: {\n      'not-found': 'The requested resource was not found.',\n      network: 'Check your internet connection and try again.',\n    },\n    statusMessages: {\n      503: 'The service is temporarily unavailable. Please try again later.',\n    },\n  },\n}\n```\n\nResolvers receive the complete normalized `OkapiError`, so they can be integrated with any i18n\nlibrary:\n\n```ts\nimport type { OkapiErrorAdapterOptions } from '@alexovn/okapi'\n\nconst options: OkapiErrorAdapterOptions = {\n  i18n: {\n    resolveTitle: ({ statusCode }) => {\n      if (statusCode === 503) {\n        return 'Temporarily unavailable'\n      }\n\n      return undefined\n    },\n    resolveMessage: ({ kind, statusCode }) => {\n      if (statusCode) {\n        return translate(`errors.http.${statusCode}`)\n      }\n\n      return translate(`errors.api.${kind}`)\n    },\n  },\n}\n```\n\n### Translation Resolution Order\n\nTitles are resolved in this order:\n\n1. `i18n.resolveTitle`\n2. `i18n.statusTitles[statusCode]`\n3. `i18n.titles[kind]`\n4. Built-in title\n\nMessages are resolved in this order:\n\n1. `i18n.resolveMessage`\n2. `i18n.statusMessages[statusCode]`\n3. `i18n.messages[kind]`\n4. Built-in message\n\nA resolver can return `undefined` to continue to the next fallback.\n\n## Custom Error Kinds\n\nApplications can extend the built-in kinds with a string union. Pass that union as the generic\nargument to options, errors, and mapper factories that need to know about the custom kinds:\n\n```ts\nimport type { OkapiErrorAdapterOptions } from '@alexovn/okapi'\nimport { OkapiError } from '@alexovn/okapi'\nimport { createFetchResponseErrorMapper } from '@alexovn/okapi/fetch'\n\ntype AppErrorKind = 'project-archived' | 'subscription-expired'\n\nconst options: OkapiErrorAdapterOptions<AppErrorKind> = {\n  resolveKind: ({ statusCode, raw }) => {\n    if (\n      statusCode === 404 &&\n      typeof raw === 'object' &&\n      raw !== null &&\n      'code' in raw &&\n      raw.code === 'PROJECT_ARCHIVED'\n    ) {\n      return 'project-archived'\n    }\n\n    return undefined\n  },\n  i18n: {\n    titles: {\n      'project-archived': 'Project archived',\n    },\n    messages: {\n      'project-archived': 'Restore the project to continue.',\n    },\n  },\n}\n\nconst mapResponseError = createFetchResponseErrorMapper(options)\n\nconst error = new OkapiError<AppErrorKind>({\n  kind: 'project-archived',\n  message: 'This project has been archived.',\n})\n```\n\n`resolveKind` runs before built-in classification and receives `source`, `statusCode`,\n`statusText`, `raw`, and `cause`. Return `undefined` to use the built-in classifier. Custom kinds\nmap to the broad `business` type by default.\n\n## Custom Validation Errors\n\nBy default, Okapi accepts validation errors shaped as a dictionary of string arrays:\n\n```ts\ntype ApiValidationErrors = Record<string, string[]>\n```\n\nUse `parseValidationErrors` when an API returns another shape. The validation-errors type is\nunconstrained, so it may be an array, dictionary, nested object, primitive, or union:\n\n```ts\nimport type { OkapiErrorAdapterOptions } from '@alexovn/okapi'\nimport { createFetchResponseErrorMapper } from '@alexovn/okapi/fetch'\n\ninterface ValidationErrorDetail {\n  code: string\n  message: string\n}\n\ninterface ArrayValidationError extends ValidationErrorDetail {\n  field?: string\n  path?: string[]\n}\n\ntype AppValidationErrors =\n  | ArrayValidationError[]\n  | Record<string, ValidationErrorDetail>\n\nconst options: OkapiErrorAdapterOptions<never, AppValidationErrors> = {\n  parseValidationErrors(value) {\n    if (Array.isArray(value)) {\n      return value as ArrayValidationError[]\n    }\n\n    if (typeof value === 'object' && value !== null) {\n      return value as Record<string, ValidationErrorDetail>\n    }\n\n    return undefined\n  },\n}\n\nconst mapResponseError = createFetchResponseErrorMapper(options)\n\nconst response = { status: 422, statusText: 'Unprocessable Content' }\nconst body = {\n  message: 'Invalid input',\n  errors: [{ field: 'email', code: 'required', message: 'Email is required' }],\n}\n\nconst mapped = mapResponseError(response, body)\nconst errors = mapped.errors // AppValidationErrors | undefined\n```\n\nThe returned value is available as both `OkapiError.validationErrors` and\n`MappedOkapiError.errors`, with its type preserved. The parser may also normalize the server value\ninto a different consumer-facing shape.\n\nProviding `parseValidationErrors` replaces the built-in parser. Return `undefined` when the value\nis not a recognized validation-error shape. If an `errors` property is present but the parser\nrejects it, Okapi treats the response as an HTTP response error instead of an API error. Omitting\nthe option preserves the default `Record<string, string[]>` behavior.\n\n## Development\n\n- Clone this repository\n- Enable Corepack using `corepack enable`\n- Install dependencies using `pnpm install`\n\n## Inspirations\n\nThis library was inspired by article\n[\"API Error Handling Demystified: Don’t Just Fetch — Handle in JS & TS\"](https://medium.com/@tanguyfab/api-error-handling-demystified-dont-just-fetch-handle-in-js-ts-7938ee22afb9)\nby [Tanguy Fabien](https://github.com/fabien-tanguy).\n\n## License\n\nMade with ❤️\n\nPublished under the [MIT license](https://github.com/alexovn/okapi/LICENSE).\n","readmeFilename":"README.md","_rev":"1-f3cc1e30542b02f9acf97ab2bde3911b"}