{"id":23701253,"url":"https://github.com/sinclairnick/unclient","last_synced_at":"2026-01-29T11:30:16.733Z","repository":{"id":269357722,"uuid":"907151945","full_name":"sinclairnick/unclient","owner":"sinclairnick","description":null,"archived":false,"fork":false,"pushed_at":"2025-01-29T05:21:16.000Z","size":43,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-29T05:25:41.774Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"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/sinclairnick.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2024-12-23T00:15:43.000Z","updated_at":"2025-01-29T05:21:19.000Z","dependencies_parsed_at":"2024-12-23T02:18:51.263Z","dependency_job_id":"f5718053-611d-4c70-91d5-bb182fd9cc4c","html_url":"https://github.com/sinclairnick/unclient","commit_stats":null,"previous_names":["sinclairnick/unclient"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Funclient","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Funclient/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Funclient/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sinclairnick%2Funclient/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sinclairnick","download_url":"https://codeload.github.com/sinclairnick/unclient/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239776720,"owners_count":19695158,"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":[],"created_at":"2024-12-30T09:35:14.939Z","updated_at":"2026-01-29T11:30:16.661Z","avatar_url":"https://github.com/sinclairnick.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"## Unclient\n\nUnclient is a simple HTTP client wrapper with an emphasis on strong type-safety, flexibilty and ergonomics. It aims to minimise boilerplate while keeping out of your way.\n\n```sh\nnpm i unclient\n```\n\n## Features\n\n- [x] Works with any HTTP API\n- [x] Flexible fetching mechanism\n- [x] Unopinionated\n- [x] Implemented in simple JavaScript (i.e. no proxy objects)\n\n## Example Usage\n\n```ts\nimport { DefineApp } from \"unclient\";\n\n// Define operations\ntype Api = DefineApi\u003c{\n  \"GET /foo/:id\": {\n    Query: {\n      foo: string;\n    };\n    Params: {\n      id: string;\n    };\n    Output: {\n      bar: string;\n    };\n  };\n}\u003e;\n\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: myFetcher,\n});\n\nconst {\n  data: { bar },\n} = await client.$fetch(\"GET /foo/:id\", {\n  query: { foo: string },\n  params: { id: \"abc\" },\n});\n\n// or: const getFoo = client.$create(\"GET /foo/:id\")\n```\n\n## Table of Contents\n\n- [Why?](#why)\n- [How?](#how)\n- [API Reference](#api-reference)\n  - [`DefineApi`](#defineapit)\n  - [`createUnclient`](#createunclientapidefopts)\n\n## Why?\n\nUsing strongly typed API clients has become a fairly common practice in modern development. However, most of the time, some sort of code generation is required, or the client is tightly coupled to some backend framework, or the fetching mechanism is overly opinionated or inflexible.\n\nUnlike these approaches, `unclient` requires no code generation, nor is it coupled to any backend framework. Additionally, fetching can be completely customised to use your desired HTTP client library (or none). This enables constructing typed clients for any HTTP backend, whether it's our own or someone elses.\n\n## Guide\n\n### Defining an API\n\nUnclient relies on an API definition and a fetcher.\n\n```ts\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher,\n});\n```\n\nThe API definition is essentially a list of HTTP operations like `GET /foo`, and a record of the various inputs for that endpoint. The input parts include:\n\n- `Params`: Path params\n- `Query`: Query params\n- `Body`: Request body\n- `Output`: Response body\n\n#### Type Definition\n\nOur API can be defined solely in at the type level:\n\n```ts\ntype Api = DefineApi\u003c{\n  \"GET /post/:id\": {\n    // ...\n  };\n}\u003e;\n\nconst client = createUnclient\u003cApi\u003e()(opts);\n```\n\n#### Runtime Definition\n\nIf we want our data to be validated on the client side, we can define our APIs like so:\n\n```ts\nconst api = defineApi(\n  \"GET /post/:id\": z.object({\n    // ...\n  })\n  // or: typebox, valibot, etc.\n)\n\nconst client = createUnclient(api)(opts);\n```\n\nIn this case, our input and output data will be validated and transformed at runtime.\n\n### Providing a Fetcher\n\nBy default, our client has no idea how to communicate with your API of interest. That behaviour is defined in the `fetcher`.\n\n```ts\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: (config) =\u003e {\n    const { path, method, query, params, body } = config;\n\n    // ...call the API\n\n    return { data: { foo: \"bar\" } };\n  },\n});\n```\n\nThe fetcher is called for each invocation of our client to a given endpoint, with specific input data, like the request body or query params. The first argument represents this information. It's up to the fetcher to decide how that information translates into an actual request, to send the request and to return any response data.\n\nFor convenience sake, `unclient` offers several fetchers out of the box. However, these are not required nor doing anything complicated. They are simply provided to further simplify setup.\n\n```ts\nimport { axiosFetcher } from \"unclient/axios\";\n// or: import { fetchFetcher } from \"unclient/fetch\";\n\nconst instance = axios.create({\n  baseUrl: \"/api/v1\",\n});\n\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: axiosFetcher({ axios: instance }),\n});\n```\n\n### Return Types\n\nAside from when throwing errors, the fetcher should always return an object with a `data` field. This will be correctly typed come time to invoke our client.\n\n```ts\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: (config) =\u003e {\n    // ...call the API\n\n    return { data };\n  },\n});\n```\n\nThis quirk enables us to return whatever additional information we want, retaining the type safety of both the API result and the additional information.\n\n```ts\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: (config) =\u003e {\n    // ...call the API\n\n    return {\n      data, // will still be typed correctly\n\n      // And so will these...\n      headers,\n      status,\n    };\n  },\n});\n```\n\n### Additional Parameters\n\nSimilarly, we can provide custom, additional parameters to our fetcher, which will be exposed when invoking our client. They can be made required or optional.\n\n```ts\nconst client = createUnclient\u003cApi\u003e()({\n  fetcher: (\n    config,\n    // Additional params:\n    requiredOption: number,\n    optionalOption?: string\n  ) =\u003e {\n    //...\n  },\n});\n```\n\n### Invoking the Client\n\nWe can invoke our client in several ways, in order to initiate an HTTP request.\n\n#### `client.$fetch`\n\nThe simplest way to make HTTP requests is to use the `$fetch` method. It accepts at least two arguments: the operation key and any input data.\n\n```ts\nconst result = await client.$fetch(\n  \"GET /foo\",\n  {\n    query: ...,\n    // ...\n  },\n\n  // + any additional fetcher options you've defined\n);\n\nconst {\n   data, // Will have type of `Output`, if specified\n   ..extra // will have type of remaining fetcher return\n } = result\n```\n\n#### `client.$create`\n\nIn certain instances we might want to reference a specific operation, avoiding the need to specify the operation key each time.\n\nWe can reference and invoke specific operations by creating functions via the `$create` method.\n\n```ts\nconst getFoo = client.$create(\"GET /foo\");\n\nconst result = await getFoo(inputs, ...additionalParams);\n```\n\n#### `client.$\u003cverb\u003e`\n\nUnclient also exposes familiar HTTP-verb-specific methods to make invocation more concise.\n\n```ts\nconst result = await client.$get(\"/foo\", inputs);\n```\n\n\u003e While the API definition supports any HTTP verb, only 'get', 'post', 'patch', 'put' and 'delete' are exposed as $methods.\n\n### OpenAPI\n\nIn conjunction with [`spec-dts`](https://github.com/sinclairnick/spec-dts), we can create OpenAPI clients without the need for any codegen.\n\nThe `unclient` API shape is entirely compatible with that produced by `spec-dts`. View the `spec-dts` instructions for more info.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairnick%2Funclient","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsinclairnick%2Funclient","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsinclairnick%2Funclient/lists"}