Graduate Program KB

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.

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.

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.

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.