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 works both in the browsers and in node.js.
const 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:`.
const author = await ketting.follow('author');
// Grab the current state
const 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 links 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 tenet 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 interaction might look like:
const ketting = new Ketting('https://api.example.org/');
const createArticle = await ketting.follow('articleCollection').follow('new'); // chained follow
const newArticle = await createArticle.post({ title: 'Hello world' });
const author = await newArticle.follow('author');
// Output author information
console.log(await author.get());
Embedded resources are a HAL feature. In situations when you are modeling 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:
const ketting = new Ketting('https://api.example.org/');
const articleCollection = await ketting.follow('articleCollection');
const items = await someCollection.followAll('item');
for (const item of items) {
console.log(await item.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.
const options = {}; // options are optional
const 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. Supported authentication methods are:
- HTTP Basic auth
- OAuth2 Bearer tokens
- OAuth2 managed client
Basic example:
const options = {
auth: {
type: 'basic',
userName: 'foo',
password: 'bar'
}
};
OAuth2 Bearer example:
const options = {
auth: {
type: 'bearer',
token: 'bar'
}
};
OAuth2 Resource Owner Password Credentials Grant example:
const options = {
auth: {
type: 'oauth2',
client: {
clientId: 'fooClient',
clientSecret: 'barSecret',
accessTokenUri: 'https://api.example.org/oauth/token',
scopes: ['test']
},
owner: {
userName: 'fooOwner',
password: 'barPassword'
}
}
};
OAuth 2 Client Credentials Grant example:
const options = {
auth: {
type: 'oauth2',
client: {
clientId: 'fooClient',
clientSecret: 'barSecret',
accessTokenUri: 'https://api.example.org/oauth/token',
scopes: ['test']
}
}
};
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 specify this as such:
const 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.
const resource = client.getResource('http://example.org'); // absolute uri
const resource = client.getResource('/foo'); // relative uri
const 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.
const someResource = await ketting.follow('author');
This is just a shortcut to:
const 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.
const 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 current uri of the resource. This is a property, not a function and is always available.
A property representing the Content-Type
of the resource. This value will
be used in GET
requests (with the Accept
header) and PUT
requests (with
the Content-Type
header).
The contentType
might be available immediately if the current resource was
followed from a link that had "type" information. If it's not available, it
might be determined later, after the first GET
request is done.
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:
const newResource = await parentResource.post({ foo: 'bar' });
// Output a list of links on the newly created resource
console.log(await newResource.links());
This function provides a really simply implementation of the PATCH
method.
All it does is encode the body to JSON and set the Content-Type
to
application/json
. I'm curious to hear use-cases for this, so open a ticket
if this doesn't cut it!
await resource.patch({
foo: 'bar'
});
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.
const 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:
const init = {
method: 'PATCH',
body: JSON.serialize(['some', 'patch', 'object'])
};
const 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 responded 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 - Human 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:
const 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:
const 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
The underlying OAuth2 client is implemented using js-client-oauth2 is exposed via the 'Ketting' class.
const ketting = new Ketting('https://api.example.org/', {
auth: {
type: 'oauth2',
client: {
clientId: 'fooClient',
clientSecret: 'barSecret',
accessTokenUri: 'https://api.example.org/oauth/token',
scopes: ['test']
},
owner: {
userName: 'fooOwner',
password: 'barPassword'
}
}
});
const oAuthClient = ketting.oauth2Helper.client;
// Interact with the underlying OAuth2 client