{"id":17609907,"url":"https://github.com/vixalien/ruhuka","last_synced_at":"2026-04-10T21:03:04.378Z","repository":{"id":57675654,"uuid":"481443141","full_name":"vixalien/ruhuka","owner":"vixalien","description":"A REST API Client","archived":false,"fork":false,"pushed_at":"2022-04-15T23:49:33.000Z","size":29,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-03T02:09:39.952Z","etag":null,"topics":["api","client","deno","javascript","rest","typescript"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vixalien.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-04-14T02:42:40.000Z","updated_at":"2022-04-14T03:29:03.000Z","dependencies_parsed_at":"2022-09-26T20:41:39.230Z","dependency_job_id":null,"html_url":"https://github.com/vixalien/ruhuka","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/vixalien/ruhuka","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vixalien%2Fruhuka","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vixalien%2Fruhuka/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vixalien%2Fruhuka/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vixalien%2Fruhuka/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vixalien","download_url":"https://codeload.github.com/vixalien/ruhuka/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vixalien%2Fruhuka/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260665335,"owners_count":23044276,"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":["api","client","deno","javascript","rest","typescript"],"created_at":"2024-10-22T17:24:06.632Z","updated_at":"2026-04-10T21:02:59.319Z","avatar_url":"https://github.com/vixalien.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ruhuka\n\nA REST API Client.\n\nIn these days, I couldn't find a viable REST API Client for Javascript so I\ncreated one. This is in it's initial days and may change very quickly. Inspired\nby [another-rest-client](https://github.com/Amareis/another-rest-client)\n\n## Installation\n\nUsing npm\n\n```sh\nnpm install ruhuka\n# or using yarn\nyarn add ruhuka\n```\n\nThen import it\n\n```js\n// commonjs\nconst { RESTResource, Resource } = require(\"ruhuka\");\n// esm\nimport RESTResource, { Resource } from \"ruhuka\";\n```\n\nUsing deno\n\n```js\nimport RESTResource, { Resource } from \"https://deno.land/x/ruhuka@v0.0.0/mod.ts\";\n```\n\n## Usage\n\nThe library exports 2 main classes: `RESTResource` and `Resource`.\n`RESTResource` is also the default export. It has some helper methods to help query\nREST APIs.\n\n### REST Resource\n\nImporting REST Resource.\n\n```js\n// import the rest resource class\nimport RESTResource from \"ruhuka\";\nimport { RESTResource } from \"ruhuka\";\n```\n\nInitialization\n\n```js\n// initialise a new resource\nconst Posts = new RESTResource(\n  \"todos\",\n  \"https://jsonplaceholder.typicode.com/posts\",\n);\n```\n\n\u003e NOTE: all returned values are `Response`s by default, so you will have to\n\u003e parse the response yourself for example by calling `posts.json()`\n\n#### CRUD\n\nThe CRUD (Create Read Update Delete) methods are only available on a\n`RESTResource` (not the `Resource`).\n\n1. Get all items in collection (READ)\n\n```js\nconst posts = await Posts.get();\n// GET https://jsonplaceholder.typicode.com/posts\n```\n\n2. Get a particular resource\n\n```js\nconst post = await Posts.get_item(\"1\");\n// GET https://jsonplaceholder.typicode.com/posts/1\n```\n\n3. Creating a post\n\n```js\nawait Posts.post({\n  userId: 1,\n  id: 101,\n  title: \"Test post\",\n  body: \"easy peasy\",\n}, {\n  encode: \"json\",\n});\n// POST https://jsonplaceholder.typicode.com/posts\n// Headers content-type=json\n// Body=JSON.stringify(body)\n```\n\n4. Updating a resource\n\n```js\n// using PUT\nawait Posts.put_item(\"1\", {\n  title: \"This is changed\",\n}, {\n  encode: \"json\",\n});\n// PUT https://jsonplaceholder.typicode.com/posts/1\n// Headers content-type=json\n// Body=JSON.stringify(body)\n\n// using PATCH\nawait Posts.put_item(\"1\", {\n  title: \"This is changed\",\n}, {\n  encode: \"json\",\n});\n// PATCH https://jsonplaceholder.typicode.com/posts/1\n// Headers content-type=json\n// Body=JSON.stringify(body)\n```\n\n5. Deleting a resource\n\n```js\nawait Posts.delete_item(\"1\");\n```\n\n### Resource\n\nImporting Resource.\n\n```js\n// import the resource class\nimport { Resource } from \"ruhuka\";\n```\n\nInitialization\n\n```js\n// initialise a new resource\nconst Posts = new Resource(\n  \"todos\",\n  \"https://jsonplaceholder.typicode.com/posts\",\n);\n```\n\nBecause a Resource does not provide helper methods such as `get`, `get_item`\netc. You have to initialise them yourself. Here is an example roughly based on\nRuby on Rails.\n\n```js\nPosts.create = Posts.new_method(\"create\", \"POST\");\nPosts.all = Posts.new_method(\"all\", \"GET\");\n// the third parameter means it's acting on a document rather than a collection in whole\nPosts.get = Posts.new_method(\"get\", \"GET\", true);\nPosts.update = Posts.new_method(\"update\", \"PUT\", true);\nPosts.delete = Posts.new_method(\"delete\", \"DELETE\", true);\n\n// Now we can use our methods\nPosts.delete(\"1\");\n```\n\n\u003e TIP: You can also use `new_method` on plain `Resource`. In fact, RESTResource is an extended version of Resource that calls `new_method` several times in the constructor to make it able to interact to a REST API.\n\n#### Configuration\n\nWhile initialising the resource, you can also set some options as outlined\nbelow:\n\n- **route**: options for the child routes (`get`,`get_item`) etc.\n\n```js\nconst ConfigedResource = new Resource(\"name\", \"uri://url\", {\n  route: { ...options },\n});\n```\n\n### Route Configuration\n\nYou can set route options per resource or per API call as shown below:\n\n```js\n// per resource\n// the third paramter\nconst JSONAPI = new Resource(\"name\", \"uri://url\", {\n  route: {\n    encode: \"json\",\n    decode: \"json\",\n    headers: {\n      \"content-type\": \"application/json\",\n      \"authorization\": \"JWT somekey\",\n    },\n  },\n});\n\n// per call\n// the first parameter on collection methods\nconst result = JSONAPI.get({\n  decode: \"text\",\n  headers: {\n    \"etag\": \"W\\\\some-etag\",\n  },\n});\n\n// the second parameter on document methods \nconst result = JSONAPI.get_item(\"1\", {\n  decode: \"text\",\n  headers: {\n    \"etag\": \"W\\\\some-etag\",\n  },\n});\n```\n\n- **encode**: `Function | \"json\" | null` Encode the request body before sending\n  it. Can be a function that receives a body and encode it to one of the\n  following types:\n  `string | Blob | BufferSource | FormData | URLSearchParams | ReadableStream\u003cUint8Array\u003e`.\n  If it is the string `\"json\"`. `JSON.stringify` will be called on the body\n  before sending it. By default the value is set to null and the body isn't\n  encoded at all.\n- **decode**: `Function | \"response\" | \"json\" | \"text\" | \"array-buffer\" | null`.\n  Converts the got Response to the given type. By default it is set to\n  `\"response\"` and therefore returns the plain `Response`.\n- **fetch**: `Function` the `fetch` function to use. By default uses\n  `globalThis.fetch`.\n- **additionalData**: `object` additional data to send in the 2nd argument to\n  `fetch`.\n- **params**:\n  `string | string[][] | Record\u003cstring, string\u003e | URLSearchParams | undefined`\n  Query search params to send (first argument to `URLSearchParams`).\n- **mustBeOk**: `boolean` the response must be in the 200-399 range\n  (successful).\n\n## Usage examples\n\n### Sample JSON API.\n\n```js\nimport { Resource, RESTResource } from \"ruhuka\";\n\nconst Posts = new RESTResource(\n  \"todos\",\n  \"https://jsonplaceholder.typicode.com/posts\",\n  {\n    route: {\n      encode: \"json\",\n      decode: \"json\",\n    },\n  },\n);\n```\n\n### Experimental nested documents\n\nGets resources attached to another resource.\n\n```js\nconst Comments = Posts.document(\"1\", \"comments\");\n\n// get all comments\nawait Comments.get();\n// get a specific comment\nawait Comments.get_item(\"1\");\n```\n\n\u003e NOTE: The nested resources are cached in case of multiple calls. The following code hence creates a single resource.\n\n```js\nconst allComments = await Posts.document(\"1\", \"comments\").get();\nconst specificComment = await Posts.document(\"1\", \"comments\").get_item(\"1\");\nawait Posts.document(\"1\", \"comments\").delete_item(specificComment.id);\n```\n\n### TypeScript\n\nYou can add 2 parameters to `Resource` to denote the type of the (decoded) data\nreturned from the document and collection APIs respectively\n\n```ts\ninterface IPost {\n  title: string;\n  body: string;\n}\n\ninterface IPaginated {\n  total: number;\n  limit: number;\n  skip: number;\n  data: IPost[];\n}\n\nconst Posts = new Resource\u003cIPost, IPaginated\u003e(\"posts\", \"uri://url\", {\n  route: {\n    encode: \"json\",\n    decode: \"json\",\n  },\n});\nconst posts: IPaginated = await Posts.get();\nconst post: IPost = await Posts.get(\"1\");\n```\n\n### Error handling\n\nThe methods return promises which can catch when an issue occur (request was\naborted, network lost) or when the result doesn't have a status code (200-399 or\n`response.ok === false`) and the route configuration option `mustBeOk` is set.\nThe package throws the original response.\n\n```js\nconst Posts = new Resource(\"posts\", \"uri://url\", {\n  route: {\n    encode: \"json\",\n    decode: \"json\",\n    mustBeOk: true,\n  },\n});\n\nconst post = await Posts.get(\"400042342\")\n  .catch(response =\u003e console.log(response.status));\n  // logs 404 (there is no post with id 400042342)\n```\n\n### Caching\n\nThe package doesn't do any caching at all. But I will work on a caching fetching function. Here is an idea of how you might implement cache.\n\n```js\n// stores a map of urls and response\nconst cache = new Map();\n\n// this implementation is very incomplete. It caches everything. In practice you would want to honor the cache headers `Cache-Control`, `ETag` etc. and would also compare the request options (changed headers may mean a different response.)\nconst cachingFetch = (url, options) =\u003e {\n  // if the url is cached, return the cached response\n  if (cache.has(url)) {\n    return cache.get(url).clone();\n  };\n  // else fetch the url then cache it\n  else {\n    return fetch(url, options)\n      .then(response =\u003e {\n        // don't cache non-ok responses\n        if (!response.ok) return response;\n        cache.set(url, response);\n        return response.clone();\n      });\n  }\n}\n\nconst Posts = new Resource(\"posts\", \"uri://url\", {\n  route: {\n    encode: \"json\",\n    decode: \"json\",\n    mustBeOk: true,\n    fetch: cachingFetch\n  },\n});\n```\n\n### Authentication\n\nYou may be required to send some headers everytime to access protected resources. It's super easy with ruhuka:\n\n```js\n// could also be a plain object\nconst headers = new Headers();\nheaders.set(\"Authorization\", `JWT YOUR_KEY_HERE`);\n\nconst Posts = new Resource(\"posts\", \"uri://url\", {\n  route: {\n    encode: \"json\",\n    decode: \"json\",\n    headers\n  },\n});\n```\n\n## Contributing\n\nNote that this is an early draft and some features are missing. However all types of pull requests are welcome.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvixalien%2Fruhuka","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvixalien%2Fruhuka","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvixalien%2Fruhuka/lists"}