{"id":16417090,"url":"https://github.com/tkrotoff/fetch","last_synced_at":"2025-03-16T17:33:53.698Z","repository":{"id":37547952,"uuid":"172324238","full_name":"tkrotoff/fetch","owner":"tkrotoff","description":"A small Fetch API wrapper","archived":false,"fork":false,"pushed_at":"2023-07-06T13:58:39.000Z","size":1533,"stargazers_count":61,"open_issues_count":2,"forks_count":9,"subscribers_count":3,"default_branch":"master","last_synced_at":"2024-10-12T07:10:55.020Z","etag":null,"topics":["fetch","fetch-api","http","http-client","http-request","request","whatwg-fetch"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tkrotoff.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-02-24T10:30:19.000Z","updated_at":"2024-10-08T17:02:56.000Z","dependencies_parsed_at":"2024-06-19T04:00:53.403Z","dependency_job_id":"53b877ef-7fd6-490e-8111-4723d43fd4e4","html_url":"https://github.com/tkrotoff/fetch","commit_stats":null,"previous_names":[],"tags_count":37,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkrotoff%2Ffetch","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkrotoff%2Ffetch/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkrotoff%2Ffetch/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tkrotoff%2Ffetch/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tkrotoff","download_url":"https://codeload.github.com/tkrotoff/fetch/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221666375,"owners_count":16860408,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["fetch","fetch-api","http","http-client","http-request","request","whatwg-fetch"],"created_at":"2024-10-11T07:11:00.420Z","updated_at":"2024-10-27T10:58:48.986Z","avatar_url":"https://github.com/tkrotoff.png","language":"TypeScript","readme":"# @tkrotoff/fetch\n\n[![npm version](https://badge.fury.io/js/%40tkrotoff%2Ffetch.svg)](https://www.npmjs.com/package/@tkrotoff/fetch)\n[![Node.js CI](https://github.com/tkrotoff/fetch/workflows/Node.js%20CI/badge.svg?branch=master)](https://github.com/tkrotoff/fetch/actions)\n[![Test Coverage](https://api.codeclimate.com/v1/badges/67aaf07dd7577e2ef340/test_coverage)](https://codeclimate.com/github/tkrotoff/fetch/test_coverage)\n[![Bundle size](https://badgen.net/bundlephobia/minzip/@tkrotoff/fetch)](https://bundlephobia.com/package/@tkrotoff/fetch)\n[![Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)\n[![Airbnb Code Style](https://badgen.net/badge/code%20style/airbnb/ff5a5f?icon=airbnb)](https://github.com/airbnb/javascript)\n\nA [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) wrapper.\n\n- Simplifies the use of Fetch\n- Tiny: less than 200 lines of code\n- No dependencies\n- Supports Node.js \u0026 web browsers\n- Comes with test utilities\n- Fully tested (against [Undici](https://github.com/nodejs/undici) \u0026 [whatwg-fetch](https://github.com/github/fetch))\n- Written in TypeScript\n\n## Why?\n\nWhen using Fetch, you must write [some](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Uploading_JSON_data) [boilerplate](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful):\n\n```JavaScript\nconst url = 'https://example.com/profile';\nconst data = { username: 'example' };\n\ntry {\n  const response = await fetch(url, {\n    method: 'POST',\n    body: JSON.stringify(data),\n    headers: {\n      'content-type': 'application/json'\n    }\n  });\n  if (!response.ok) {\n    throw new Error('Network response was not ok');\n  }\n  const json = await response.json();\n  console.log('Success:', json);\n} catch (e) {\n  console.error('Error:', e);\n}\n```\n\nWith @tkrotoff/fetch it becomes:\n\n```JavaScript\ntry {\n  const response = await postJSON(url, data).json();\n  console.log('Success:', response);\n} catch (e /* HttpError | TypeError | DOMException */) {\n  console.error('Error:', e);\n}\n```\n\nYou don't have to worry about:\n\n- HTTP headers: Accept and Content-Type are already set\n- stringifying the request body\n- One `await` instead of two\n- No need to manually throw an exception on HTTP error status (like 404 or 500)\n\n## Usage\n\nExamples:\n\n- https://stackblitz.com/github/tkrotoff/fetch/tree/codesandbox.io/examples/web\n- https://stackblitz.com/github/tkrotoff/fetch/tree/codesandbox.io/examples/node\n- https://github.com/tkrotoff/MarvelHeroes\n\n`npm install @tkrotoff/fetch`\n\n```JavaScript\nimport { defaults, postJSON } from '@tkrotoff/fetch';\n\ndefaults.init = { /* ... */ };\n\nconst response = await postJSON(\n  'https://jsonplaceholder.typicode.com/posts',\n  { title: 'foo', body: 'bar', userId: 1 }\n).json();\n\nconsole.log(response);\n```\n\nOr copy-paste [Http.ts](src/Http.ts) into your source code.\n\n## JavaScript runtimes support\n\n@tkrotoff/fetch supports Node.js and modern browsers\n\n### Node.js\n\n- Nothing is needed if Node.js \u003e= 18.0\n- Use [`--experimental-fetch`](https://nodejs.org/docs/latest-v16.x/api/cli.html#--experimental-fetch) if Node.js \u003e= 16.15 \u003c 18.0\n- ⚠️ [node-fetch](https://github.com/node-fetch/node-fetch) is not supported with @tkrotoff/fetch \u003e= 0.17 due to [`Request` class limitations](https://github.com/node-fetch/node-fetch/blob/v3.3.1/README.md#class-request)\n\nCheck [examples/node](examples/node)\n\n### Browsers\n\nCheck [examples/web](examples/web)\n\n## API\n\n- `get(input:` [`RequestInfo`](https://fetch.spec.whatwg.org/#requestinfo)` | URL, init?:` [`RequestInit`](https://fetch.spec.whatwg.org/#requestinit)`): ResponsePromiseWithBodyMethods`\n\n- `post(input: RequestInfo | URL, body?:` [`BodyInit`](https://fetch.spec.whatwg.org/#bodyinit)`, init?: RequestInit): ResponsePromiseWithBodyMethods`\n- `postJSON(input: RequestInfo | URL, body: object, init?: RequestInit): ResponsePromiseWithBodyMethods`\n\n- `put(input: RequestInfo | URL, body?: BodyInit, init?: RequestInit): ResponsePromiseWithBodyMethods`\n- `putJSON(input: RequestInfo | URL, body: object, init?: RequestInit): ResponsePromiseWithBodyMethods`\n\n- `patch(input: RequestInfo | URL, body?: BodyInit, init?: RequestInit): ResponsePromiseWithBodyMethods`\n- `patchJSON(input: RequestInfo | URL, body: object, init?: RequestInit): ResponsePromiseWithBodyMethods`\n\n- `del(input: RequestInfo | URL, init?: RequestInit): ResponsePromiseWithBodyMethods`\n\n- `isJSONResponse(response: `[`Response`](https://fetch.spec.whatwg.org/#response)`): boolean`\n\n`ResponsePromiseWithBodyMethods` being `Promise\u003c`[`Response`](https://fetch.spec.whatwg.org/#response)`\u003e` with added methods from [`Body`](https://fetch.spec.whatwg.org/#body-mixin).\n\n### HttpError\n\n@tkrotoff/fetch throws [`HttpError`](src/HttpError.ts) with [`response`](https://fetch.spec.whatwg.org/#response) and [`request`](https://fetch.spec.whatwg.org/#request) properties when the HTTP status code is \u003c `200` or \u003e= `300`.\n\n### Test utilities\n\n- `createResponsePromise(body?:` [`BodyInit`](https://fetch.spec.whatwg.org/#bodyinit)`, init?:` [`ResponseInit`](https://fetch.spec.whatwg.org/#responseinit)`): ResponsePromiseWithBodyMethods`\n- `createJSONResponsePromise(body: object, init?: ResponseInit): ResponsePromiseWithBodyMethods`\n\n- `createHttpError(body: BodyInit, status: number, statusText?: string): HttpError`\n- `createJSONHttpError(body: object, status: number, statusText?: string): HttpError`\n\n### HttpStatus\n\nInstead of writing HTTP statuses as numbers `201`, `403`, `503`... you can replace them with [`HttpStatus`](src/HttpStatus.ts) and write more explicit code:\n\n```TypeScript\nimport { HttpStatus } from '@tkrotoff/fetch';\n\nconsole.log(HttpStatus._201_Created);\nconsole.log(HttpStatus._403_Forbidden);\nconsole.log(HttpStatus._503_ServiceUnavailable);\n\ntype HttpStatusEnum = typeof HttpStatus[keyof typeof HttpStatus];\nconst status: HttpStatusEnum = HttpStatus._200_OK;\n```\n\n### Configuration\n\n@tkrotoff/fetch exposes `defaults.init` that will be applied to every request.\n\n```TypeScript\nimport { defaults } from '@tkrotoff/fetch';\n\ndefaults.init.mode = 'cors';\ndefaults.init.credentials = 'include';\n```\n\n## Testing\n\nWhen testing your code, use `createResponsePromise()` and `createJSONResponsePromise()`:\n\n```TypeScript\nimport * as Http from '@tkrotoff/fetch';\n\n// https://github.com/aelbore/esbuild-jest/issues/26#issuecomment-968853688\n// https://github.com/swc-project/swc/issues/5059\njest.mock('@tkrotoff/fetch', () =\u003e ({\n  __esModule: true,\n  ...jest.requireActual('@tkrotoff/fetch')\n}));\n\ntest('OK', async () =\u003e {\n  const mock = jest.spyOn(Http, 'get').mockImplementation(() =\u003e\n    Http.createResponsePromise('test')\n  );\n\n  const response = await Http.get(url).text();\n  expect(response).toEqual('test');\n\n  expect(mock).toHaveBeenCalledTimes(1);\n  expect(mock).toHaveBeenCalledWith(url);\n\n  mock.mockRestore();\n});\n\ntest('fail', async () =\u003e {\n  const mock = jest.spyOn(Http, 'get').mockImplementation(() =\u003e\n    Http.createResponsePromise(\n      '\u003c!DOCTYPE html\u003e\u003ctitle\u003e404\u003c/title\u003e',\n      { status: 404, statusText: 'Not Found' }\n    )\n  );\n\n  await expect(Http.get(url).text()).rejects.toThrow('Not Found');\n\n  expect(mock).toHaveBeenCalledTimes(1);\n  expect(mock).toHaveBeenCalledWith(url);\n\n  mock.mockRestore();\n});\n```\n\nOther possible syntax with `jest.mock` instead of `jest.spyOn`:\n\n```TypeScript\nimport { createResponsePromise, get } from '@tkrotoff/fetch';\n\nbeforeEach(() =\u003e jest.resetAllMocks());\n\njest.mock('@tkrotoff/fetch', () =\u003e ({\n  ...jest.requireActual('@tkrotoff/fetch'),\n  get: jest.fn(),\n  post: jest.fn(),\n  postJSON: jest.fn(),\n  put: jest.fn(),\n  putJSON: jest.fn(),\n  patch: jest.fn(),\n  patchJSON: jest.fn(),\n  del: jest.fn()\n}));\n\ntest('OK', async () =\u003e {\n  jest.mocked(get).mockImplementation(() =\u003e\n    createResponsePromise('test')\n  );\n\n  const response = await get(url).text();\n  expect(response).toEqual('test');\n\n  expect(get).toHaveBeenCalledTimes(1);\n  expect(get).toHaveBeenCalledWith(url);\n});\n\ntest('fail', async () =\u003e {\n  jest.mocked(get).mockImplementation(() =\u003e\n    createResponsePromise(\n      '\u003c!DOCTYPE html\u003e\u003ctitle\u003e404\u003c/title\u003e',\n      { status: 404, statusText: 'Not Found' }\n    )\n  );\n\n  await expect(get(url).text()).rejects.toThrow('Not Found');\n\n  expect(get).toHaveBeenCalledTimes(1);\n  expect(get).toHaveBeenCalledWith(url);\n});\n```\n\nCheck [examples/node](examples/node) and [examples/web](examples/web).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkrotoff%2Ffetch","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftkrotoff%2Ffetch","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftkrotoff%2Ffetch/lists"}