The HAL+JSON Resource Object with TypeScript support.
3.2.0
npm install halson --savebower install halson --savevar halson = require('halson');
var embed = halson({
title: "joyent / node",
description: "evented I/O for v8 javascript"
})
.addLink('self', '/joyent/node')
.addLink('author', {
href: '/joyent',
title: 'Joyent'
});
var resource = halson({
title: "john doe",
username: "doe",
emails: [
"john.doe@example.com",
"doe@example.com"
]
})
.addLink('self', '/doe')
.addEmbed('starred', embed);
console.log(resource.title);
console.log(resource.emails[0]);
console.log(resource.getLink('self'));
console.log(resource.getEmbed('starred'));
console.log(JSON.stringify(resource));HALSON supports TypeScript with generic typing for strongly-typed HAL+JSON resources. This allows you to work with your domain objects while preserving all HAL+JSON functionality.
import halson from "halson";
// Define your domain interface
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Create a typed resource
const userData: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
isActive: true
};
// halson<T>() returns HALSONResource<T> which includes both
// your domain properties AND all HAL+JSON methods
const userResource = halson<User>(userData)
.addLink('self', '/users/1')
.addLink('edit', '/users/1/edit');
// ✅ Fully typed access to your domain properties
console.log(userResource.name); // string
console.log(userResource.isActive); // boolean
// ✅ Plus all HAL+JSON functionality
console.log(userResource.getLink('self')); // HALSONLink | undefined
console.log(userResource.listLinkRels()); // string[]Option 1: Import with namespace access
import halson from "halson";
const resource = halson<User>(userData);
type UserResource = halson.HALSONResource<User>;Option 2: Import with wildcard
import * as halson from "halson";
const resource = halson<User>(userData);
type UserResource = halson.HALSONResource<User>;Option 3: Type-only imports (recommended)
import halson from "halson";
import type { HALSONResource } from "halson";
const resource = halson<User>(userData);
type UserResource = HALSONResource<User>;interface Repository {
id: number;
name: string;
language: string;
stars: number;
}
interface UserWithRepos {
id: number;
name: string;
email: string;
}
const userWithRepos = halson<UserWithRepos>({
id: 1,
name: "John Doe",
email: "john@example.com"
})
.addLink('self', '/users/1')
.addEmbed('repositories', [
halson<Repository>({
id: 101,
name: "awesome-project",
language: "TypeScript",
stars: 42
}).addLink('self', '/repos/101'),
halson<Repository>({
id: 102,
name: "another-project",
language: "JavaScript",
stars: 23
}).addLink('self', '/repos/102')
]);
// ✅ Type-safe access to embedded resources
const repos = userWithRepos.getEmbeds<Repository>('repositories');
repos.forEach(repo => {
console.log(`${repo.name} (${repo.language}) - ${repo.stars} stars`);
});// Generic function overloads
function halson(data?: string | object): HALSONResource;
function halson<T extends object>(data: string | T): HALSONResource<T>;
// Generic type alias
type HALSONResource<T extends object = object> = HALSONResource & T;Existing JavaScript code works unchanged:
// This JavaScript code continues to work exactly the same
const resource = halson({
title: "Hello World",
count: 42
});
console.log(resource.title); // "Hello World"When migrating to TypeScript, you can gradually add type annotations:
// Step 1: Add interface
interface Post {
title: string;
count: number;
}
// Step 2: Add generic type parameter
const resource = halson<Post>({
title: "Hello World",
count: 42
});
// Now you get full type safety!
console.log(resource.title); // ✅ TypeScript knows this is a stringHALSON now provides enterprise-grade HATEOAS features similar to Spring HATEOAS and other Java frameworks:
You can use either namespace access or named imports for the advanced features:
// Approach 1: Namespace access (all examples below use this)
import halson from "halson";
const resource = halson<User>(userData);
type UserResource = halson.HALSONResource<User>;
type PagedUsers = halson.PagedResource<UserList>;
const validation = resource.validate(options);// Approach 2: Named imports (alternative style)
import halson, {
HALSONResource,
PagedResource,
IanaRels,
ValidationOptions
} from "halson";
const resource = halson<User>(userData);
type UserResource = HALSONResource<User>;
type PagedUsers = PagedResource<UserList>;
const validation = resource.validate({
requireLinks: [IanaRels.SELF]
});import halson from "halson";
// Rich link objects with metadata
const resource = halson<User>(userData)
.addLink(halson.IanaRels.SELF, {
href: "/users/1",
type: "application/json",
title: "User Profile",
method: "GET"
})
.addLink(halson.IanaRels.EDIT, {
href: "/users/1",
method: "PUT",
type: "application/json",
title: "Edit User"
});
// Link existence checking
if (resource.hasLink(halson.IanaRels.EDIT)) {
const editUrl = resource.getHref(halson.IanaRels.EDIT);
}// Add templated links
resource.addTemplate('search', '/users{?name,email,status}');
// Expand templates with variables
const searchUrl = resource.expandTemplate('search', {
name: 'John',
status: 'active'
});
// Result: "/users?name=John&status=active"// Async navigation following links
const userOrders = await userResource.follow<Order>('orders');
const allRelated = await userResource.followAll<Resource>('related');
// URL resolution
const nextPageUrl = resource.resolve('next', { page: 2, size: 10 });// Fluent resource construction
const resource = halson.HALResourceBuilder<User>(userData)
.link(halson.IanaRels.SELF, '/users/1')
.link(halson.IanaRels.EDIT, {
href: '/users/1',
method: 'PUT',
type: 'application/json'
})
.template('search', '/users{?q}')
.curie('acme', 'https://api.acme.com/rels/{rel}', true)
.embed('profile', profileResource)
.build();interface UserList {
users: User[];
}
type PagedUsers = halson.PagedResource<UserList>;
const pagedUsers: PagedUsers = {
users: [...],
page: {
number: 0,
size: 10,
totalElements: 100,
totalPages: 10
},
// HAL methods + pagination helpers
hasNext: () => true,
hasPrev: () => false,
next: () => '/users?page=1',
prev: () => null
};// Add namespace support
resource
.addCurie('acme', 'https://api.acme.com/rels/{rel}', true)
.addLink('acme:orders', '/users/1/orders')
.addLink('acme:preferences', '/users/1/preferences');
// Expand compact URIs
const expandedRel = resource.expandCurie('acme:orders');
// Result: "https://api.acme.com/rels/orders"// Using namespace access
const validation = resource.validate({
strict: true,
requireLinks: [halson.IanaRels.SELF],
allowMissingLinks: [halson.IanaRels.NEXT]
});
if (!validation.valid) {
console.error('Validation errors:', validation.errors);
console.warn('Validation warnings:', validation.warnings);
}// Alternative: Using named imports
import halson, { IanaRels, ValidationOptions } from "halson";
const options: ValidationOptions = {
strict: true,
requireLinks: [IanaRels.SELF],
allowMissingLinks: [IanaRels.NEXT]
};
const validation = resource.validate(options);// Built-in content negotiation helpers
if (resource.accepts('application/json')) {
const json = resource.asJson();
}
const halJson = resource.asHal();
const contentType = resource.getContentType();halson.IanaRels.SELF // 'self'
halson.IanaRels.EDIT // 'edit'
halson.IanaRels.DELETE // 'delete'
halson.IanaRels.NEXT // 'next'
halson.IanaRels.PREV // 'prev'
halson.IanaRels.FIRST // 'first'
halson.IanaRels.LAST // 'last'
halson.IanaRels.RELATED // 'related'
halson.IanaRels.ALTERNATE // 'alternate'
halson.IanaRels.CANONICAL // 'canonical'
halson.IanaRels.COLLECTION // 'collection'
halson.IanaRels.ITEM // 'item'
// ... and moreCreate a new HAL+JSON Resource Object.
data(optional): Initial data as serialized string or Object.
// empty HAL+JSON Resource Object
var resource = halson();
// resource from a serialized data
var resource = halson('{title:"Lorem Ipsum",_links:{self:{href:"/ipsum"}}');
// resource from an Object
resource = halson({
_links: {
self: {
href: {"/ipsum"}
}
},
title: "Lorem Ipsum"
});
// resource from another resource (no-op)
var resourceX = halson(resource);
console.log(resource === resourceX); // trueList all link relations.
var data = {
_links: {
self: {href: '/doe'},
related: [
{href: 'http://doe.com'},
{href: 'https://twitter.com/doe'}
]
}
}
var resource = halson(data);
console.log(resource.listLinkRels()); // ['self', 'related']List all link relations.
var data = {
_embedded: {
starred: {
_links: {
self: {href: '/joyent/node'}
}
title: "joyent / node",
description: "evented I/O for v8 javascript"
}
}
}
var resource = halson(data);
console.log(resource.listEmbedRels()); // ['starred']Get all links with relation rel.
rel(required): Relation name.filterCallback(optional): Function used to filter array of links. docbegin,end(optional): slice filtered links. doc
var twitterLinks = resource.getLinks('related', function(item) {
return item.name === "twitter";
});Get first link with relation rel.
rel(required): Relation name.filterCallback(optional): Function used to filter array of links. docdefault(optional): Default value if the link does not exist.
var firstRelatedLink = resource.getLink('related');Get all embedded resources with relation rel.
rel(required): Relation name.filterCallback(optional): Function used to filter array of embeds. docbegin,end(optional): slice filtered links. doc
var embeds = resource.getEmbeds('starred');Get first embedded resource with relation rel.
rel(required): Relation name.filterCallback(optional): Function used to filter array of embeds. docdefault(optional): Default value if the link does not exist.
var nodeProject = resource.getEmbed('starred', function(embed) {
return embed.getLink('self', {}).href === '/joyent/node';
});Add a link with relation rel.
rel(required): Relation name.link(required): Link to be added (string or Object).
resource
.addLink('related', 'http://doe.com')
.addLink('related', {
href: 'https://twitter.com/doe',
name: 'twitter'
});Add a nested resource with relation rel.
rel(required): Relation name.embed(required): Resource to be embedded (Object or HALSONResource).
var embed = {
_links: {
self: {href: '/joyent/node'}
},
title: "joyent / node"
}
resource.addEmbed('starred', embed);Add a nested resource with relation rel.
rel(required): Relation name.index(required): Index number where embed will be insertedembed(required): Resource to be embedded (Object or HALSONResource).
var embed = {
_links: {
self: {href: '/joyent/node'}
},
title: "joyent / node"
};
resource.addEmbed('starred', embed); // add embed
var embed2 = {
_links: {
self: {href: '/joyent/node'}
},
title: "joyent / node"
};
resource.insertEmbed('starred', 0, embed2); // insert new embed before first itemRemove links with relation rel. If filterCallback is not defined, all links with relation rel will be removed.
rel(required): Relation name.filterCallback(optional): Function used to filter array of links. doc
// remove links with relation 'related' and name 'twitter'
resource.removeLinks('related', function(link) {
return link.name === "twitter";
});Remove embedded resources with relation rel. If filterCallback is not defined, all embeds with relation rel will be removed.
rel(required): Relation name.filterCallback(optional): Function used to filter array of links. doc
// remove embedded resources with relation 'starred' and self-link '/koajs/koa'
resource.removeLinks('starred', function(embed) {
return embed.getLink('self', {}).href === '/koajs/koa';
});- Marek Fojtl jerrymf@gmail.com (Author)
- Martin Jurča martin.jurca@firma.seznam.cz
- Juraj Hájovský juraj@hajovsky.sk
- Tom Ruttle truttle@bmj.com
- David Kalosi david.kalosi@gmail.com
- José Santos Martins Pereira
- Jonelle Amio
Thank you to all contributors who have helped improve HALSON!