Skip to content

beuss/simple-hal-client

Repository files navigation

Simple HAL client

This is a simple implementation of HAL-FORMS client. It is mostly tested against Spring HATEOS.

Basic usage

Simply create a client and start fetching resources from your remote backend. The starting point of every interaction is fetch, which you should use to get the root of the API, supposed to provide links to every part of it.

const client = new SimpleHalClient('http://my-server.example/api');
const response = await client.fetch();

Response can be used as a regular fetch response. It just provides an additional hal() method whose goal is to parse the returned document as HAL(-FORMS). If the document cannot be parsed, the method will throw (as well as if the returned response is not valid json). Otherwise, you get back an HalFormsResource instance allowing you to navigate through links and relationships.

Accessing to resource properties

Except for _links, _embedded and _templates, all properties of the resources are accessible through the property data of an HalFormsResource. The class is parametrized to describe the type of the data. e.g.:

interface People {
	lastName: string;
	firstName: string;
}



const resource: HalFormsResource<People> = await response.hal();
const people = resource.data; // people: People

Navigating links

One of the benefits of using HAL is to attach links to resources (through the _links property of each HAL resource. Using SimpleHalClient, fetching and following a link is as simple as:

let resource: HalFormsResource<any>;


const ancestorLink: HalLink = resource.link('ancestor')!;
const ancestorResource = await ancestorLink.follow('ancestor');

const childrenLinks: HalLink[] = resource.links('children')!;
const childrenResource = childrenLinks.map(l => l.follow());

Links can be mono or multivalued. Trying to get multivalued link through link will throw and so will trying to get a monovalued link through links.

You can fetch all links for a given resource using links() (without argument). The relationship between the link and its resource is then accessible through the rel property.

Using embedded

Another feature of HAL is to embedded resources in an enclosing resource. The goal is, among others, to prevent multiple fetches or to give a consistent view of a resource (think about database transaction). Each embedded element can be the canonical representation of the embedded resource, a partial representation or something having nothing to do with this canonical representation. If the embedded resource has a link with self key, you should be able to fetch its canonical representation by following it.

Getting an embedded resource is as simple as:

let resource: HalFormsResource<any>;


const ancestorResource = resource.embedded('ancestor')!;
const childrenResource = resource.embeddeds('children')!;

Note that, as for the links, attempting to get a multivalued embedded with embedded will throw and so will fetching a monovalued embedded through embedded.

An embedded resource is a full HAL-FORMS resource, it can in turn have links, embedded and templates.

Using templates

Templates is an HAL-FORMS feature. They describe actions that can be performed on a resource. Each template has an identifying name and may describe the needed properties to send in the request.

Using templates with this lib looks like:

let resource: HalFormsResource<any>;


const addChildTemplate = resource.template('addChild')!;
const childResource = addChildTemplate.invoke(JSON.stringify({ name: 'Foo', age: 28 }));

The spec does not specify (sic) whether templates names are unique. This library assumes they are.

Filters

You can add filters to the client, in order to pre-process request or post-process response. This can be useful for e.g. authentication handling (if server answer 401, you may want to renew an access token or redirect user to login) or object creation.

Here is an example of filter that turns 204 (no content) responses into the resource pointed by the returned location header (you can test it with Spring HATEOAS examples:

const client = new SimpleHalClient('https://simple-hal-client.example/employees');
this.client.appendFilter(async (params: FilterParams) => {
  const response = await params.next(params.request);
  if(response.status === 204 && response.headers.has('location')) {
    const body: ReadableStream = response.body as ReadableStream;
    return params.client.fetch(response.headers.get('location')!);
  }
  return response;
});




const response = await client.fetch();
const employees = await response.hal();
const template = employees.template('default')!;
const newEmployeeResource = await template.invoke(JSON.stringify({lastName: 'Foo'})).hal();