backbone.hateoas is a Javascript library which facilitates the use of HATEOAS (Hypermedia as the Engine of Application State) and the HAL (Hypertext Application Language) standard in your Backbone and Marionette applications.
To understand what is HATEAOS and what's its place in REST in general we advise you to understand the Richardson Maturity Model and most specifically the REST Level 3 - Hypermedia Controls.
Today several standards exists to model REST resources and links between them - HAL, JSON-LD, Collection+JSON, SIREN to name a few.
backbone.hateoas uses the HAL standard because its JSON format is easy to manipulate with Backbone and because we think its the most used one today.
The HAL specification is quickly described here HAL - Hypertext Application Language, the JSON version of HAL has also been published as an internet draft here : JSON Hypertext Application Language.
The HAL standard defines a generic resource concept, but Backbone defines 2 kinds of "resources" (the models and the collections).
So backbone.hateoas defines 2 kinds of resources :
Hal.Model
class defines a generic HAL resource and is very similar to the
Backbone.Model
class ;
Hal.Collection
class is a specialized HAL resource dedicated to manipulation of
collection resources, this one is a little opinionated (but its design is based on REST API best
practices).
The Hal.Model
class extends the Backbone.Model
class and adds support to
manipulate HAL links and HAL embedded resources.
// Instanciation of an Hal.Model object is done the same way as you're used to with a standard Backbone model
var user = new Hal.Model({
firstName: "John",
lastName: "Doe",
_links: {
avatar: {
href: "http://localhost/api/users/1/avatar.png"
}
},
_embedded: {
address: {
"city" : "Paris",
"country" : "France",
"street" : "142 Rue de Rivoli",
"zip" : "75001",
"_links" : {
"self" : {
"href" : "http://localhost/api/addresses/1"
}
}
}
}
});
// "Direct" properties are accessed using standard Backbone methods
user.get('firstName');
user.get('lastName');
// Getting a link, a link can be manipulated like a Backbone.Model
user.getLink('avatar').get('href');
// Getting an embedded resource, an embedded resource is a Hal.Model
user.getEmbedded('address').get('city');
user.getEmbedded('address').getLink('self').get('href');
The Hal.Model
class has been designed to be very easy to manipulate, we've chosen to create
getLink(rel)
and getEmbedded(rel)
methods to force the developpers to clearly
show their intention in the code.
For more informations abount the Hal.Model
class please read the chapter
Hal.Model.
The Hal.Collection
class extends the Backbone.Collection
class by adding
utility methods to navigate through an "HAL Collection". The HAL standard does not describe the concept
of an HAL collection but most framwork do it (
Apigility,
Willdurand Hateoas, etc.). The
Hal.Collection
class is configurable and can be used to work with custom pagination
parameter names and links.
For more informations abount the Hal.Collection
class please read the chapter
Hal.Collection.
The easiest way to use the library is to pull it with
Bower by adding the following dependency inside your
bower.json
file.
{
"dependencies": {
"backbone.hateoas" : "~0.1"
}
}
The HAL standard defines 2 new media types which are application/hal+json
and
application/hal+xml
, it also indicates the following :
When serving HAL over HTTP, the Content-Type of the response should contain the relevant media type name.
So it defines how the server should respond but is does not define how we should send HTTP POST, PUT and PATCH requests. In fact this is something which seems to be left unspecified deliberatly.
The Hal.contentType
global configuration parameter allows to configure how your models and
collections should be serialized before being sent to the server (i.e it changes the behavior of the
Backbone Model.toJSON()
and Collection.toJSON()
methods).
The current version of the library supports 2 content types : application/json
and
application/hal+json
.
application/json
The following sample ...
// The 'Hal.contentType' parameter is an application wide parameter
// So in most cases it is only configured at one place in your project
Hal.contentType = 'application/json';
...
// Somewhere in the code of the application
var john = new Hal.Model();
john.urlMiddle = 'users';
john.set('id', 'jdoe');
john.fetch({
success : function() {
console.log(user.toJSON());
}
});
Will display ...
{
"id": 1,
"firstName": "John",
"lastName" : "Doe",
"address" : {
"city" : "Paris",
"country" : "France",
"street" : "142 Rue de Rivoli",
"zip" : "75001"
}
}
application/hal+json
The following sample ...
// The 'Hal.contentType' parameter is an application wide parameter
// So in most cases it is only configured at one place in your project
Hal.contentType = 'application/hal+json';
...
// Somewhere in the code of the application
var john = new Hal.Model();
john.urlMiddle = 'users';
john.set('id', 'jdoe');
john.fetch({
success : function() {
console.log(user.toJSON());
}
});
Will display ...
{
"id": 1,
"firstName": "John",
"lastName" : "Doe",
"_embedded" : {
"address" : {
"city" : "Paris",
"country" : "France",
"street" : "142 Rue de Rivoli",
"zip" : "75001",
"_links" : {
"self" : {
"href" : "http://localhost/backbone.hateoas/test/api/addresses/1"
}
}
}
},
"_links" : {
"address" : {
"href" : "http://localhost/backbone.hateoas/test/api/addresses/1"
},
"self" : {
"href" : "http://localhost/backbone.hateoas/test/api/users/1"
}
}
}
When you work with HAL you often have a root URL which is global to your API, in most cases this URL is the root of a catalog (i.e a special resource describing the documentation of your endpoints and how to access them).
The Hal.urlRoot
property allows you to configure this global API root URL (please note that
the name urlRoot
has been chosen to be the same as the Backbone Model urlRoot
parameter).
For exemple if our API is located at https://myserver.com/api
then we could declare the
following Hal.urlRoot
.
Hal.urlRoot = 'https://myserver.com/api';
After that Hal.Collection
and Hal.Model
can automatically generate absolute
API urls for you.
var users = new Hal.Collection();
users.urlMiddle = 'users';
// Fetch 'https://myserver.com/api/users'
users.fetch();
// Hal.Model can also be used easily without any associated collection
// This will fetch 'https://myserver.com/api/users/1'
var john = new Hal.Model();
john.urlMiddle = 'users';
john.set('id', 1);
john.fetch();
// If you want you can "force" use of other absolute URLs
// This will fetch 'https://myserver2.com/api/users'
users.url = 'https://myserver2.com/api/users';
users.fetch();
// This will fetch 'https://myserver2.com/api/users/jdoe'
john.urlRoot = 'https://myserver2.com/api/users/jdoe';
john.fetch();
The urlMiddle
is an additional URL parameter specific to backbone.hateoas and which easier
model fetching without being forced to attach your model to a collection.
The urlMiddle
is used only when your model is not linked to a collection having a URL and
which do not define a specific urlRoot
property.
The urlMiddle
is used to create an absolute URL (with the url()
method) equal
to the concatenation of the Hal.urlRoot
property plus the urlMiddle
.
Here is an exemple :
Hal.urlRoot = 'https://myserver.com/api';
// This will fetch 'https://myserver.com/api/users/1'
var john = new Hal.Model();
john.urlMiddle = 'users';
john.set('id', 1);
john.fetch();
// This will fetch 'https://myserver.com/api/companies/2/users/1'
john = new Hal.Model();
john.urlMiddle = 'companies/2/users';
john.set('id', 1);
john.fetch();
By default the Backbone toJSON([options])
method do not accept or use any option, a
Hal.Model
can use an additional contentType
option which can be equal to
application/json
or application/hal+json
.
When the toJSON([options])
method is called with this option it overwrites the
Hal.contentType
configuration only for this call.
In most cases this is useful when you when to store a serialized version of a resource somewhere (in the local storage for example).
The backbone.hateoas Hal.Collection
class is an opinionated class to manipulate HAL
collection resources, what we call a HAL collection is a resource having the following structure :
{
"page" : 1,
"page_count" : 6,
"page_size" : 12,
"total_items" : 65,
"_links" : {
"self" : {
"href" : "http://myserver.com/api/users?page=3"
},
"first" : {
"href" : "http://myserver.com/api/users?page=1"
},
"last" : {
"href" : "http://myserver.com/api/users?page=133"
},
"previous" : {
"href" : "http://myserver.com/api/users?page=2"
},
"next" : {
"href" : "http://myserver.com/api/users?page=4"
}
},
"_embedded" : {
"users" : [
{ ... },
{ ... },
...
]
}
}
The constraints imposed to have a valid HAL Collection are the following :
So a Hal.Collection
object automatically manages paginated collections, the name of the
attributes page
, page_count
, page_size
and
total_items
have been chosen to be compliant with
Apigility.
var UserCollection = Hal.Collection.extend({
model : function(attrs, options) {
if(attrs.type === 'STUDENT') {
return new Student(attrs);
} else if(attrs.type === 'TEACHER') {
return new Teacher(attrs);
}
},
rel : 'users',
url : 'http://myserver.com/api/users'
});
var users = new UserCollection();
users.getFirstPage();
users.getLastPage();
users.getPreviousPage();
users.getNextPage();
hasEmbedded(rel)
and hasLink(rel)
methods on the
Hal.Model
class ;
clone()
method for Hal.Model
;toJSON()
method, the contentType
option was not propagated
to embedded resources.
toJSON()
method to take a contentType
option to configure how to
serialize a resource, this is useful when you when to serialize an HAL resource to persist it in the
local storage for example.
toJSON()
methods to return plain JSON or HAL JSON ;Hal.contentType
parameter to configure the behavior of the
toJSON()
methods ;
middleUrl
parameter but it was
urlMiddle
;
Hal.urlRoot
parameter ;urlMiddle
parameter to be used with Hal.Collection
and
Hal.Model
;
toJSON()
method implementations ;Hal.Model
class is initialized in the set(key, val, options)
method instead of initialize(options)
;
_links
parsing and management ;_embedded
parsing and management ;