Introduction to HAL
Hypermedia as the Engine of Application State (HATEOAS) is a RESTful design constraint that dictates that a client should be able to navigate a web application through hypermedia links. HATEOAS allows for decoupled systems and self-discoverable APIs, which makes it easier to modify and evolve the API over time. The Halson library is a lightweight library for creating and consuming HAL (Hypertext Application Language) documents, which is a JSON-based media type for representing hypermedia links. In this tutorial, we'll explore how to use the Halson library to create and consume HAL documents with HATEOAS.
Halson
The Halson npm library is a lightweight and flexible library that provides a simple way to represent and manipulate HAL (Hypertext Application Language) resources in JavaScript. HAL is a simple format that allows you to express hypermedia controls, such as links, in a structured way.
Halson provides a number of features that make it easy to work with HAL resources, including:
- Parsing: Halson can parse HAL resources from JSON or JavaScript objects.
- Serialization: Halson can serialize HAL resources to JSON.
- Link manipulation: Halson makes it easy to add, remove, and modify links in a HAL resource.
- Embedded resource manipulation: Halson allows you to add, remove, and modify embedded resources in a HAL resource.
Halson does not support the following features:
- Templating: Halson does not support the use of templates to generate new HAL resources.
- URI templates: Halson does not support the use of URI templates to generate new URIs.
- Curies: Halson does not support the use of CURIEs (Compact URIs) for link relations.
- Immutable resources: Halson does not provide a way to create immutable HAL resources, which can be useful for caching or for creating snapshots of resources.
- Following links: Halson does not provide functionality to follow links to request other HAL documents
Overall, Halson is a useful library for working with HAL resources in JavaScript. It provides a clean and intuitive API that makes it easy to work with HAL resources, and it is flexible enough to handle a wide range of use cases.
Setting Up
Before we start coding, let's set up our project. First, create a new directory for our project, and initialize it as a Node.js project with npm init
.
mkdir introduction-to-halson
cd introduction-to-halson
npm init -y
Next, install the Halson and Jest libraries as dependencies.
npm install halson jest --save-dev
Now we're ready to start coding!
Writing Tests
We'll start by writing some tests for our code using Jest. We'll write our tests first, following the Test-Driven Development (TDD) approach. This ensures that our code is testable and that we have a clear understanding of what our code should do.
Create a new directory called __tests__
at the root of our project, and create a new file inside it called hateoas.test.js. In this file, we'll write our tests.
const halson = require('halson');
describe('HATEOAS', () => {
test('should return a HAL document with a self link', () => {
// TODO: write test
});
test('should return a HAL document with a link to related resource', () => {
// TODO: write test
});
});
Creating a HAL Document
Let's start by writing the first test. We want to create a HAL document that includes a link to itself. We'll use the halson
function from the Halson library to create our HAL document.
const halson = require('halson');
describe('HATEOAS', () => {
test('should return a HAL document with a self link', () => {
const hal = halson({ name: 'John Doe' });
hal.addLink('self', '/users/1');
expect(hal).toEqual({
_links: {
self: {
href: '/users/1'
}
},
name: 'John Doe'
});
});
});
In this test, we're creating a HAL document using the halson
function, passing in an object with a name
property. We're then adding a link to the HAL document using the addLink
method, passing in the relation type self
and the URI of the resource. Finally, we're using Jest's expect
function to check that the resulting HAL document is what we expect it to be.
Adding a Link to a Related Resource
Let's write the second test. We want to create a HAL document that includes a link to a related resource. We'll use the addLink
method from the Halson library to create and add the link to the HAL document.
const halson = require('halson');
describe('HATEOAS', () => {
test('should return a HAL document with a self link', () => {
const hal = halson({ name: 'John Doe' });
hal.addLink('self', '/users/1');
expect(hal).toEqual({
_links: {
self: {
href: '/users/1'
}
},
name: 'John Doe'
});
});
test('should return a HAL document with a link to related resource', () => {
const hal = halson({ name: 'John Doe' });
const related = {href: '/posts'};
hal.addLink('related', related);
expect(hal).toEqual({
_links: {
related: {
href: '/posts'
}
},
name: 'John Doe'
});
});
});
In this test, we're creating a HAL document using the halson
function, passing in an object with a name
property. We're then creating a link to a related resource as an object with a href
property set to the URI of the resource. We're then adding the link to the HAL document using the addLink
method, passing in the relation type related
and the link we just created. Finally, we're using Jest's expect
function to check that the resulting HAL document is what we expect it to be.
Adding an Embedded Resource
We want to create a document that includes an embedded resource, We'll use the addEmbed
method to add the embed to the document.
const halson = require('halson');
describe('HATEOAS', () => {
test('should return a HAL document with an embedded resource', () => {
const hal = halson({ name: 'John Doe' });
const embed = {
_links: {
self: {href: '/profile'}
},
aboutMe: "Some guy called John"
};
hal.addEmbed('profile', embed);
expect(hal).toEqual({
_embedded: {
profile: {
_links: {
self: {href: '/profile'}
},
aboutMe: "Some guy called John"
}
},
name: 'John Doe'
})
});
});
In this test, we're creating a HAL document using the halson
function, passing in an object with a name
property. We're then creating a resource to be embedded and then embedding the resource in the HAL document using the addEmbed
method, passing in the relation type profile
and the resource we just created. Finally, we're using Jest's expect
function to check that the resulting HAL document is what we expect it to be.
Consuming a HAL Document
Now that we've written tests for creating HAL documents with links, let's write some tests for consuming HAL documents with links. We'll use Jest's beforeEach
function to set up some common variables that we'll use in our tests.
const halson = require('halson');
describe('HATEOAS', () => {
let hal;
beforeEach(() => {
hal = halson({
_links: {
self: {
href: '/users/1'
},
related: [
{ href: '/posts' },
{ href: '/profile' }
]
},
_embedded: {
profile: {
_links: {
self: {href: '/profile'}
},
aboutMe: "Some guy called John"
}
},
name: 'John Doe'
});
});
test('should get a list of link relations', () => {
// TODO: write test
})
test('should get the self link', () => {
// TODO: write test
});
test('should get the first related link', () => {
// TODO: write test
});
test('should get all the related links', () => {
// TODO: write test
});
test('should get a list of embedded relations', () => {
// TODO: write test
});
test('should get an embedded resource', () => {
// TODO: write test
});
});
We're using the halson
function from the Halson library to parse a JSON object representing a HAL document with links. We're then storing the resulting HAL object in a variable called hal
, which we'll use in our tests.
Getting the Link Relations
We want to write tests for listing the relations we can query links for.
const halson = require('halson');
describe('HATEOAS', () => {
let hal;
beforeEach(() => {
hal = halson({
_links: {
self: {
href: "/users/1",
},
related: [{ href: "/posts" }, { href: "/profile" }],
},
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
test('should get a list of link relations', () => {
const relations = hal.listLinkRels();
expect(relations).toEqual(['self', 'related']);
})
});
In this test we're using the listLinkRels
method to get the list of link relations from the HAL document. We're then using Jest's expect
function to check the resulting relations are what we expect.
Getting a Link
We want to write tests for getting the self link and the related links from the HAL document.
const halson = require('halson');
describe('HATEOAS', () => {
let hal;
beforeEach(() => {
hal = halson({
_links: {
self: {
href: "/users/1",
},
related: [{ href: "/posts" }, { href: "/profile" }],
},
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
test('should get the self link', () => {
const self = hal.getLink('self');
expect(self).toEqual({
href: '/users/1'
});
});
test('should get the first related link', () => {
const related = hal.getLink("related");
expect(related).toEqual({
href: "/posts",
});
});
test('should get all the related links', () => {
const related = hal.getLinks("related");
expect(related).toEqual([
{href: "/posts"},
{href: "/profile"},
]);
});
});
In this test, we're using the getLink
method to get the self link from the HAL document. We're then using Jest's expect
function to check that the resulting link is what we expect it to be.
Getting a List of Embedded Relations
We want to write tests for listing the relations we can query embedded resources for.
describe('HATEOAS', () => {
beforeEach(() => {
hal = halson({
_links: {
self: {
href: "/users/1",
},
related: [{ href: "/posts" }, { href: "/profile" }],
},
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
test('should get a list of embedded relations', () => {
const relations = hal.listEmbedRels()
expect(relations).toEqual(['profile'])
});
})
In this test we're using the listEmbedRels
method to get the list of embedded relations from the HAL document. We're then using Jest's expect
function to check the resulting relations are what we expect.
Getting an Embedded Resource
We want to write tests for getting an embedded resource from the document.
describe('HATEOAS', () => {
beforeEach(() => {
hal = halson({
_links: {
self: {
href: "/users/1",
},
related: [{ href: "/posts" }, { href: "/profile" }],
},
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
test("should get an embedded resource", () => {
const embed = hal.getEmbed("profile");
expect(embed).toEqual({
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
});
});
})
In this test we're using the getEmbed
method to get the embedded resource with the profile
relationship from the document.We're then using Jest's expect
function to check the resulting object is what we expect.
Putting it All Together
Here's the complete code for our HATEOAS tests using the Halson library in JavaScript:
const halson = require("halson");
describe("HATEOAS", () => {
describe("creating HAL documents", () => {
test("should return a HAL document with a self link", () => {
const hal = halson({ name: "John Doe" });
hal.addLink("self", "/users/1");
expect(hal).toEqual({
_links: {
self: {
href: "/users/1",
},
},
name: "John Doe",
});
});
test("should return a HAL document with a link to related resource", () => {
const hal = halson({ name: "John Doe" });
const related = { href: "/posts" };
hal.addLink("related", related);
expect(hal).toEqual({
_links: {
related: {
href: "/posts",
},
},
name: "John Doe",
});
});
test("should return a HAL document with an embedded resource", () => {
const hal = halson({ name: "John Doe" });
const embed = {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
};
hal.addEmbed("profile", embed);
expect(hal).toEqual({
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
});
describe("querying HAL documents", () => {
let hal;
beforeEach(() => {
hal = halson({
_links: {
self: {
href: "/users/1",
},
related: [{ href: "/posts" }, { href: "/profile" }],
},
_embedded: {
profile: {
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
},
},
name: "John Doe",
});
});
test("should get a list of link relations", () => {
const relations = hal.listLinkRels();
expect(relations).toEqual(["self", "related"]);
});
test("should get the first related link", () => {
const related = hal.getLink("related");
expect(related).toEqual({
href: "/posts",
});
});
test("should get all the related links", () => {
const related = hal.getLinks("related");
expect(related).toEqual([{ href: "/posts" }, { href: "/profile" }]);
});
test("should get a list of embedded relations", () => {
const relations = hal.listEmbedRels();
expect(relations).toEqual(["profile"]);
});
test("should get an embedded resource", () => {
const embed = hal.getEmbed("profile");
expect(embed).toEqual({
_links: {
self: { href: "/profile" },
},
aboutMe: "Some guy called John",
});
});
});
});
In this code, we're creating a HAL document with links using the addLink
method. We're then testing that the HAL document is what we expect it to be using Jest's expect
function. We're also testing that we can get a link from the HAL document using the getLink
method and getting embedded resources using getEmbed
method. We also tested that we can add and query embedded resources on documents.
Conclusion
In this tutorial, we've learned how to use the Halson library in JavaScript to work with HATEOAS APIs. We started by learning what HATEOAS is and how it can be used in APIs. We then installed the Halson library and used it to create a HAL document with links. We tested our code using Jest, and learned how to get a link from a HAL document using the getLink
method.
HATEOAS is an important concept in modern API design, and using a library like Halson can make working with HATEOAS APIs easier. By writing tests for our code, we can be sure that our code is working as expected and that we're following best practices for API design.