The Ketting library is an attempt at creating a 'generic' hypermedia client, it supports an opinionated set of modern features REST services might have.
The library supports HAL, Web Linking (HTTP Link Header) and HTML5 links. It uses the Fetch API and is meant for client and server-side javascript.
var ketting = new Ketting('https://api.example.org/');
// Follow a link with rel="author". This could be a HTML5 `<link>`, a
// HAL `_links` or a HTTP `Link:`.
var author = await ketting.follow('author');
// Grab the current state
var authorState = await author.get();
// Change the firstName property of the object. Note that this assumes JSON.
authorState.firstName = 'Evert';
// Save the new state
await author.put(authorState);
npm install ketting
or:
yarn add ketting
Ketting is a library that sits on top of a Fetch API to provide a RESTful interface and make it easier to follow REST best practices more strictly.
It provides some useful abstractions that make it easier to work with true
hypermedia / HATEAOS servers. It currently parses HAL and has a deep
understanding of inks and embedded resources. There's also support for parsing
and following links from HTML documents, and it understands the HTTP Link:
header.
Using this library it becomes very easy to follow links from a single bookmark, and discover resources and features on the server.
One core tennet of building a good REST service, is that URIs should be
discovered, not hardcoded in an application. It's for this reason that the
emphasis in this library is not on URIs (like most libraries) but on
relation-types (the rel
) and links.
Generally when interacting with a REST service, you'll want to only hardcode a single URI (a bookmark) and discover all the other APIs from there on on.
For example, consider that there is a some API at https://api.example.org/
.
This API has a link to an API for news articles (rel="articleCollection"
),
which has a link for creating a new article (rel="new"
). When POST
ing on
that uri, the api returns 201 Created
along with a Location
header pointing
to the new article. On this location, a new rel="author"
appears
automatically, pointing to the person that created the article.
This is how that iteraction might look like:
var ketting = new Ketting('https://api.example.org/');
var createArticle = await ketting.follow('articleCollection').follow('new'); // chained follow
var newArticle = await createArticle.post({title: 'Hello world'});
var author = await newArticle.follow('author');
// Output author information
console.log(await author.get());
Embedded resources are a HAL feature. In situations when you are modelling a
'collection' of resources, in HAL you should generally just create links to
all the items in the collection. However, if a client wants to fetch all these
items, this can result in a lot of HTTP requests. HAL uses _embedded
to work
around this. Using _embedded
a user can effectively tell the HAL client about
the links in the collection and immediately send along the contents of those
resources, thus avoiding the overhead.
Ketting understands _embedded
and completely abstracts them away. If you use
ketting with a HAL server, you can therefore completely ignore them.
For example, given a collection resource with many resources that hal the
relationshiptype item
, you might use the following API:
var ketting = new Ketting('https://api.example.org/');
var articleCollection = await ketting.follow('articleCollection');
var items = await someCollection.followAll('item');
for (i in items) {
console.log(await items[i].get());
}
Given the last example, if the server did not use embedding, it will result in a HTTP GET request for every item in the collection.
If the server did use embedding, there will only be 1 GET request.
A major advantage of this, is that it allows a server to be upgradable. Hot paths might be optimized using embedding, and the client seamlessly adjusts to the new information.
Further reading:
If your server emits application/problem+json documents (rfc7807) on HTTP errors, the library will automatically extract the information from that object, and also provide a better exception message (if the title property is provided).
Ketting works on any stable node.js version and modern browsers. To run Ketting in a browser, the following must be supported by a browser:
- The Fetch API.
- Promises (async/await is not required)
The 'Ketting' class is the main class you'll use to access anything else.
var options = {}; // options are optional
var ketting = new Ketting('https://api.example.org/', options);
2 keys or options
are currently supported: auth
and fetchInit
. auth
can be used to specify authentication information. HTTP Basic auth and OAUth2
Bearer token are
supported.
Basic example:
var options = {
auth: {
type: 'basic',
userName: 'foo',
password: 'bar'
}
};
Bearer example:
var options = {
auth: {
type: 'bearer',
token: 'bar'
}
};
The fetchInit
option is a default list of settings that's automatically
passed to fetch()
. This is especially useful in a browser, where there's a
few more settings highly relevant to the security sandbox.
For example, to ensure that the browser automatically passed relevant cookies to the endpoint, you would specifiy this as such:
var options = {
fetchInit : {
credentials: 'include'
}
};
Other options that you may want to set might be mode
or cache
. See the
documentation for the Request constructor
for the full list.
Return a 'resource' object, based on it's url. If the url is not supplied, a resource will be returned pointing to the bookmark.
If a relative url is given, it will be resolved based on the bookmark uri.
var resource = client.getResource('http://example.org'); // absolute uri
var resource = client.getResource('/foo'); // relative uri
var resource = client.getResource(); // bookmark
The resource is returned immediately, and not as a promise.
The follow
function on the Ketting
follows a link based on it's relation
type from the bookmark resource.
var someResource = await ketting.follow('author');
This is just a shortcut to:
var someResource = await ketting.getResource().follow('author');
The fetch
function is a wrapper for the new Fetch web standard. This
function takes the same arguments (input
and init
), but it decorates the
HTTP request with Authentication headers.
var response = await ketting.fetch('https://example.org');
The Resource
class is the most important object, and represents a REST
resource. Functions such follow
and getResource
always return Resource
objects.
Returns the result of a GET
request. This function returns a Promise
.
await resource.get();
If the resource was fetched earlier, it will return a cached copy.
Updates the resource with a new representation
await resource.put({ 'foo' : 'bar' });
Deletes the resource.
await resource.delete();
This function returns a Promise that resolves to null
.
This function is meant to be an easy way to create new resources. It's not
necessarily for any type of POST
request, but it is really meant as a
convenience method APIs that follow the typical pattern of using POST
for
creation.
If the HTTP response from the server was successful and contained a Location
header, this method will resolve into a new Resource. For example, this might
create a new resource and then get a list of links after creation:
var newResource = await parentResource.post({ foo: 'bar' });
// Output a list of links on the newly created resource
console.log(await newResource.links());
The refresh
function behaves the same as the get()
function, but it ignores
the cache. It's equivalent to a user hitting the "refresh" button in a browser.
This function is useful to ditching the cache of a specific resource if the server state has changed.
console.log(await resource.refresh());
Returns a list of Link
objects for the resource.
console.log(await resource.links());
});
You can also request only the links for a relation-type you are interested in:
resource.links('author'); // Get links with rel=author
Follows a link, by it's relation-type and returns a new resource for the target.
var author = await resource.follow('author');
console.log(await author.get());
The follow
function returns a special kind of Promise that has a follow()
function itself.
This makes it possible to chain follows:
resource
.follow('author')
.follow('homepage')
.follow('icon');
Lastly, it's possible to follow RFC6570 templated links (templated URI), using the second argument.
For example, a link specified as:
{ href: "/foo{?a}", templated: true}
May be followed using
resource
.follow('some-templated-link', { a: 'bar'})
This would result following a link to the /foo?a=bar
uri.
This method works like follow()
but resolves into a list of resources.
Multiple links with the same relation type can appear in resources; for
example in collections.
var items = await resource.followAll('item');
console.log(items);
The fetch
function is a wrapper for the Fetch API
. It takes very similar
arguments to the regular fetch, but it does a few things special:
- The uri can be omitted completely. If it's omitted, the uri of the resource is used.
- If a uri is supplied and it's relative, it will be resolved with the uri of the resource.
For example, this is how you might do a HTTP PATCH
request:
var init = {
method: 'PATCH',
body: JSON.serialize(['some', 'patch, 'object'])
};
var response = await resource.fetch(init);
console.log(response.statusCode);
This function is identical to fetch
, except that it will throw a (async)
exception if the server responsed with a HTTP error.
The link class represents any Link of any type of document. It has the following properties:
- rel - relation type
- href - The uri
- baseHref - the uri of the parent document. Used for resolving relative uris.
- type - A mimetype, if specified
- templated - If it's a URI Template. Most of the time this is false.
- title - Hunman readable label for the uri
- name - Unique identifier for the link within the document (rarely used).
Returns the absolute uri to the link. For example:
var link = new Link({href: '/foo', baseHref: "http://example.org/bar" });
console.log(link.resolve());
// output is http://example.org/foo
Expands a templated link. Example:
var link = new Link({href: 'http://example.org/foo{?q}', templated: true});
console.log(link.expand({q: 'bla bla'});
// output is http://example.org/foo?q=bla+bla