Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/vixalien/ruhuka
A REST API Client
https://github.com/vixalien/ruhuka
api client deno javascript rest typescript
Last synced: 26 days ago
JSON representation
A REST API Client
- Host: GitHub
- URL: https://github.com/vixalien/ruhuka
- Owner: vixalien
- Created: 2022-04-14T02:42:40.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2022-04-15T23:49:33.000Z (over 2 years ago)
- Last Synced: 2024-10-16T15:04:01.477Z (about 1 month ago)
- Topics: api, client, deno, javascript, rest, typescript
- Language: TypeScript
- Homepage:
- Size: 28.3 KB
- Stars: 0
- Watchers: 2
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# ruhuka
A REST API Client.
In these days, I couldn't find a viable REST API Client for Javascript so I
created one. This is in it's initial days and may change very quickly. Inspired
by [another-rest-client](https://github.com/Amareis/another-rest-client)## Installation
Using npm
```sh
npm install ruhuka
# or using yarn
yarn add ruhuka
```Then import it
```js
// commonjs
const { RESTResource, Resource } = require("ruhuka");
// esm
import RESTResource, { Resource } from "ruhuka";
```Using deno
```js
import RESTResource, { Resource } from "https://deno.land/x/[email protected]/mod.ts";
```## Usage
The library exports 2 main classes: `RESTResource` and `Resource`.
`RESTResource` is also the default export. It has some helper methods to help query
REST APIs.### REST Resource
Importing REST Resource.
```js
// import the rest resource class
import RESTResource from "ruhuka";
import { RESTResource } from "ruhuka";
```Initialization
```js
// initialise a new resource
const Posts = new RESTResource(
"todos",
"https://jsonplaceholder.typicode.com/posts",
);
```> NOTE: all returned values are `Response`s by default, so you will have to
> parse the response yourself for example by calling `posts.json()`#### CRUD
The CRUD (Create Read Update Delete) methods are only available on a
`RESTResource` (not the `Resource`).1. Get all items in collection (READ)
```js
const posts = await Posts.get();
// GET https://jsonplaceholder.typicode.com/posts
```2. Get a particular resource
```js
const post = await Posts.get_item("1");
// GET https://jsonplaceholder.typicode.com/posts/1
```3. Creating a post
```js
await Posts.post({
userId: 1,
id: 101,
title: "Test post",
body: "easy peasy",
}, {
encode: "json",
});
// POST https://jsonplaceholder.typicode.com/posts
// Headers content-type=json
// Body=JSON.stringify(body)
```4. Updating a resource
```js
// using PUT
await Posts.put_item("1", {
title: "This is changed",
}, {
encode: "json",
});
// PUT https://jsonplaceholder.typicode.com/posts/1
// Headers content-type=json
// Body=JSON.stringify(body)// using PATCH
await Posts.put_item("1", {
title: "This is changed",
}, {
encode: "json",
});
// PATCH https://jsonplaceholder.typicode.com/posts/1
// Headers content-type=json
// Body=JSON.stringify(body)
```5. Deleting a resource
```js
await Posts.delete_item("1");
```### Resource
Importing Resource.
```js
// import the resource class
import { Resource } from "ruhuka";
```Initialization
```js
// initialise a new resource
const Posts = new Resource(
"todos",
"https://jsonplaceholder.typicode.com/posts",
);
```Because a Resource does not provide helper methods such as `get`, `get_item`
etc. You have to initialise them yourself. Here is an example roughly based on
Ruby on Rails.```js
Posts.create = Posts.new_method("create", "POST");
Posts.all = Posts.new_method("all", "GET");
// the third parameter means it's acting on a document rather than a collection in whole
Posts.get = Posts.new_method("get", "GET", true);
Posts.update = Posts.new_method("update", "PUT", true);
Posts.delete = Posts.new_method("delete", "DELETE", true);// Now we can use our methods
Posts.delete("1");
```> 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.
#### Configuration
While initialising the resource, you can also set some options as outlined
below:- **route**: options for the child routes (`get`,`get_item`) etc.
```js
const ConfigedResource = new Resource("name", "uri://url", {
route: { ...options },
});
```### Route Configuration
You can set route options per resource or per API call as shown below:
```js
// per resource
// the third paramter
const JSONAPI = new Resource("name", "uri://url", {
route: {
encode: "json",
decode: "json",
headers: {
"content-type": "application/json",
"authorization": "JWT somekey",
},
},
});// per call
// the first parameter on collection methods
const result = JSONAPI.get({
decode: "text",
headers: {
"etag": "W\\some-etag",
},
});// the second parameter on document methods
const result = JSONAPI.get_item("1", {
decode: "text",
headers: {
"etag": "W\\some-etag",
},
});
```- **encode**: `Function | "json" | null` Encode the request body before sending
it. Can be a function that receives a body and encode it to one of the
following types:
`string | Blob | BufferSource | FormData | URLSearchParams | ReadableStream`.
If it is the string `"json"`. `JSON.stringify` will be called on the body
before sending it. By default the value is set to null and the body isn't
encoded at all.
- **decode**: `Function | "response" | "json" | "text" | "array-buffer" | null`.
Converts the got Response to the given type. By default it is set to
`"response"` and therefore returns the plain `Response`.
- **fetch**: `Function` the `fetch` function to use. By default uses
`globalThis.fetch`.
- **additionalData**: `object` additional data to send in the 2nd argument to
`fetch`.
- **params**:
`string | string[][] | Record | URLSearchParams | undefined`
Query search params to send (first argument to `URLSearchParams`).
- **mustBeOk**: `boolean` the response must be in the 200-399 range
(successful).## Usage examples
### Sample JSON API.
```js
import { Resource, RESTResource } from "ruhuka";const Posts = new RESTResource(
"todos",
"https://jsonplaceholder.typicode.com/posts",
{
route: {
encode: "json",
decode: "json",
},
},
);
```### Experimental nested documents
Gets resources attached to another resource.
```js
const Comments = Posts.document("1", "comments");// get all comments
await Comments.get();
// get a specific comment
await Comments.get_item("1");
```> NOTE: The nested resources are cached in case of multiple calls. The following code hence creates a single resource.
```js
const allComments = await Posts.document("1", "comments").get();
const specificComment = await Posts.document("1", "comments").get_item("1");
await Posts.document("1", "comments").delete_item(specificComment.id);
```### TypeScript
You can add 2 parameters to `Resource` to denote the type of the (decoded) data
returned from the document and collection APIs respectively```ts
interface IPost {
title: string;
body: string;
}interface IPaginated {
total: number;
limit: number;
skip: number;
data: IPost[];
}const Posts = new Resource("posts", "uri://url", {
route: {
encode: "json",
decode: "json",
},
});
const posts: IPaginated = await Posts.get();
const post: IPost = await Posts.get("1");
```### Error handling
The methods return promises which can catch when an issue occur (request was
aborted, network lost) or when the result doesn't have a status code (200-399 or
`response.ok === false`) and the route configuration option `mustBeOk` is set.
The package throws the original response.```js
const Posts = new Resource("posts", "uri://url", {
route: {
encode: "json",
decode: "json",
mustBeOk: true,
},
});const post = await Posts.get("400042342")
.catch(response => console.log(response.status));
// logs 404 (there is no post with id 400042342)
```### Caching
The 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.
```js
// stores a map of urls and response
const cache = new Map();// 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.)
const cachingFetch = (url, options) => {
// if the url is cached, return the cached response
if (cache.has(url)) {
return cache.get(url).clone();
};
// else fetch the url then cache it
else {
return fetch(url, options)
.then(response => {
// don't cache non-ok responses
if (!response.ok) return response;
cache.set(url, response);
return response.clone();
});
}
}const Posts = new Resource("posts", "uri://url", {
route: {
encode: "json",
decode: "json",
mustBeOk: true,
fetch: cachingFetch
},
});
```### Authentication
You may be required to send some headers everytime to access protected resources. It's super easy with ruhuka:
```js
// could also be a plain object
const headers = new Headers();
headers.set("Authorization", `JWT YOUR_KEY_HERE`);const Posts = new Resource("posts", "uri://url", {
route: {
encode: "json",
decode: "json",
headers
},
});
```## Contributing
Note that this is an early draft and some features are missing. However all types of pull requests are welcome.