aixigo / hal-http-client

A status code driven JSON HAL HTTP client based on the fetch API.
MIT License
3 stars 2 forks source link

hal-http-client

A status code driven JSON HAL HTTP client based on the fetch API.

The API doc can be found here.

What is HAL and where does this client come into play?

HAL (Hypertext Application Layer), as the name already states, is an application level layer on top of JSON or XML that provides conventions for linking between different resources. For RESTful APIs it helps to reach level 3 of the Richardson Maturity Model . Additionally it defines how to embed related resources into the response of their parent resource for efficient APIs and lowering the need for actual HTTP requests over the wire.

The hal-http-client defines a convenient API for accessing resources represented as HAL on top of JSON (XML is currently not supported and there are no plans in doing so):

Templated URLs (RFC6570) are supported via the url-template library.

Installation and Usage

npm install hal-http-client

Basically import the library, make a new instance and start discovering resources by making a GET request to the root URL of your API.

import * as halHttp from '../hal-http-client';

const hal = halHttp.create( {
   headers: { 'accept-language': 'de' },
   on: {
      // globally define how responses for status codes should be handled that were not handled locally
      '5xx': ( data, response ) => {
         console.error( `Caught unhandled 5xx error (status: ${response.status})` );
      }
   }
} );
hal.get( 'http://my-server/api/root' )
// See the example below on how to go on from here

Example

Lets take the following simple model of a person and his cars from the specs:

// person
{
   "name": "Peter",
   "age": 34
}
// his address
{
   "street": "Mainstreet 12",
   "postalCode": "12345",
   "city": "Faketown"
}
// his cars
[
   {
      "type": "VW",
      "model": "T3 Vanagon"
   },
   {
      "type": "DMC",
      "model": "DeLorean"
   }
]

An according JSON response with all sub resources embedded could look like this:

{
   "name": "Peter",
   "age": 34,
   "_links": {
      "self": { "href": "peter" },
      "address": { "href": "peter/address" },
      "cars": { "href": "peter/cars" }
   },
   "_embedded": {
      "address": {
         "street": "Mainstreet 12",
         "postalCode": "12345",
         "city": "Faketown",
         "_links": { "self": { "href": "peter/address" } }
      },
      "cars": {
         "_links": {
            "self": { "href": "peter/cars" },
            "car": [ { "href": "peter/cars/0" }, { "href": "peter/cars/1" } ]
         },
         "_embedded": {
            "car": [
               {
                  "type": "VW",
                  "model": "T3 Vanagon",
                  "_links": { "self": { "href": "peter/cars/0" } }
               },
               {
                  "type": "DMC",
                  "model": "DeLorean",
                  "_links": { "self": { "href": "peter/cars/1" } }
               }
            ]
         }
      }
   }
}

Getting the cars starting from the person can be achieved in the following way:

hal.get( '/peter' )
   .on( {
      '200': hal.thenFollow( 'cars' )
   } )
   .on( {
      '200': hal.thenFollowAll( 'car' )
   } )
   .on( {
      '200': carList => {
         console.log( carList );
         // will print:
         /*
           [
               {
                  "type": "VW",
                  "model": "T3 Vanagon",
                  "_links": { "self": { "href": "peter/cars/0" } }
               },
               {
                  "type": "DMC",
                  "model": "DeLorean",
                  "_links": { "self": { "href": "peter/cars/1" } }
               }
            ]
         */
      }
   } );

The hal.thenFollow method is a convenience factory for creating a function, which takes the response of the previous request, then follows the given relation in the response body and finally returns a ResponsePromise for further on chaining. So instead this part could have been written the following way:

hal.get( '/peter' )
   .on( {
      '200': ( peterResource, response ) => {
         return hal.follow( peterResource, 'cars' );
      }
   } )

The method hal.thenFollowAll basically works the same, only that it expects to find an array of relations to follow on the given resource and return all results again as an array.

Optional relations

For some resources a relation may be optional. If it is sufficient to just check whether the relation is available or not, the function halHttp.canFollow() is the right choice. Simply pass it the representation as first and the relation to test as second argument:

halHttp.canFollow( peterResource, 'cars' ); // => true
halHttp.canFollow( peterResource, 'job' ); // => false

If on the other hand two different tasks should be executed in either case, the "virtual" status code 'norel' can instead be used as an on-handler:

hal.follow( peterResource, 'work' )
   .on( {
      '200': () => {
         console.log( 'Peter currently is employed' );
      },
      'norel': () => {
         console.log( 'Peter currently is unemployed' );
      }
   } );

From here on you should consult the API doc to have a look at all the other APIs available on the hal-http-client.