Client library for HATEOAS level 3 RESTful APIs that provide hypermedia controls
npm install @mountainpass/waychaserClient library for HATEOAS level 3 RESTful APIs that provide hypermedia controls using:
- Link (RFC8288) and Link-Template headers.
- HAL
- Siren
This isomorphic library is compatible with Node.js 12.x, 14.x and 16.x, Chrome, Firefox, Safari and Edge.
|
Node.js | 
Chrome | 
Firefox | 
Safari | 
Edge | 
iOS |
Android |
| -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 12.x, 14.x, 16.x | latest version | latest version | latest version | latest version | latest version | latest version |

- waychaser
- ToC
- Usage
- Node.js
- Browser
- Getting the response
- Getting the response body
- Requesting linked resources
- Multiple links with the same relationship
- Forms
- Query forms
- Path parameter forms
- Request body forms
- DELETE, POST, PUT, PATCH
- Examples
- HAL
- Siren
- Upgrading from 1.x to 2.x
- Removal of Loki
- Operation count
- Finding operations
- Upgrading from 2.x to 3.x
- Accept Header
- Handlers
- Error responses
- Invoking missing operations
- Handling location headers
- Upgrading from 3.x to 4.x
- Problem vs WayChaserResponse
``bash`
npm install @mountainpass/waychaser
`js
import { WayChaser } from '@mountainpass/waychaser'
//...
const waychaser = new WayChaser()
try {
const apiResource = await waychaser.load(apiUrl)
// do something with apiResourceerror
} catch (error) {
// do something with `
}
`html
type="text/javascript"
src="https://unpkg.com/@mountainpass/waychaser@5.0.11"
>
...
`
WayChaser makes it's http requests using fetch and the Fetch.Response is available via the response property.
For example
`js`
const responseUrl = apiResource.response.url
WayChaser makes the response body available via the body() async method.
For example
`js`
const responseUrl = await apiResource.body()Requesting linked resources
Level 3 REST APIs are expected to return links to related resources. WayChaser expects to find these links via RFC 8288 link headers, link-template headers, HAL _link elements or Siren link elements.
WayChaser provides methods to simplify requesting these linked resources.
For instance, if the apiResource we loaded above has a next link like any of the following:
Link header:
``
Link: `
HALjson`
{
"_links": {
"next": { "href": "https://api.waychaser.io/example?p=2" }
}
}`
Sirenjson`
{
"links": [
{ "rel": [ "next" ], "href": "https://api.waychaser.io/example?p=2" },
]
}
then that next page can be retrieved using the following code
`js`
const nextResource = await apiResource.invoke('next')
You don't need to tell waychaser whether to use Link headers, HAL _links or Siren links; it will figure it out application/hal+json
based on the resource's media-type. If the media-type is if will try to parse the links in the _link property of the body. If the media-type is application/vnd.siren+json if will try to parse the links in the link property of the body.
Regardless of the resource's media-type, it will always try to parse the links in the Link and Link-Template
headers.
Resources can have multiple links with the same relationship, such as
HAL
`json`
{
"_links": {
"item": [{
"href": "/first_item",
"name": "first"
},{
"href": "/second_item",
"name": "second"
}]
}
}
If you know the name of the resource, then waychaser can load it using the following code
`js`
const firstResource = await apiResource.invoke({ rel: 'item', name: 'first' })
Support for query forms is provided via:
- RFC6570 URI Templates in:
- link-template headers, and
- HAL _links.
For instance if our resource has either of the following
Link-Template header:
``
Link-Template: `
HALjson`
{
"_links": {
"search": { "href": "https://api.waychaser.io/search{?q}" }
}
}
Then waychaser can execute a search for "waychaser" with the following code
`js`
const searchResultsResource = await apiResource.invoke('search', {
q: 'waychaser'
})
Support for query forms is provided via:
- RFC6570 URI Templates in:
- link-template headers, and
- HAL _links.
For instance if our resource has either of the following
Link-Template header
``
Link-Template: `
HALjson`
{
"_links": {
"item": { "href": "https://api.waychaser.io/users{/username}" }
}
}
Then waychaser can retrieve the user with the username "waychaser" with the following code
`js`
const userResource = await apiResource.invoke('item', {
username: 'waychaser'
})
Support for request body forms is provided via:
- An extended form of link-template headers, and
- Siren actions.
To support request body forms with link-template headers, waychaser
supports three additional parameters in the link-template header:method
- - used to specify the HTTP method to useparams*
- - used to specify the fields the form expectsaccept*
- - used to specify the media-types that can be used to send the body as per,application/x-www-form-urlencoded
RFC7231 and defaulting to
If our resource has either of the following:
Link-Template header:
``
Link-Template:
rel="https://waychaser.io/rels/create-user";
method="POST";
params*=UTF-8'en'%7B%22username%22%3A%7B%7D%7D'
If your wondering what the UTF-8'en'%7B%22username%22%3A%7B%7D%7D' part is, it's just the JSON {"username":{}}
encoded as an Extension Attribute as per
(RFC8288) Link Headers. Don't worry, libraries like
http-link-header can do this encoding for you.
Siren
`json`
{
"actions": [
{
"name": "https://waychaser.io/rels/create-user",
"href": "https://api.waychaser.io/users",
"method": "POST",
"fields": [
{ "name": "username" }
]
}
]
}username
Then waychaser can create a new user with the "waychaser" with the following code
`js`
const createUserResultsResource = await apiResource.invoke('https://waychaser.io/rels/create-user', {
username: 'waychaser'
})
NOTE: The URL https://waychaser.io/rels/create-user in the above code is NOT the end-point the form is https://api.waychaser.io/users
posted to. That URL is a custom Extension Relation that identifies
the semantics of the operation. In the example above, the form will be posted to
As mentioned above, waychaser supports Link and Link-Template headers that include method properties,
to specify the HTTP method the client must use to execute the relationship.
For instance if our resource has the following link
Link header:
``
Link:
Then the following code
`js`
const deletedResource = await apiResource.invoke('https://waychaser.io/rel/delete')
will send a HTTP DELETE to https://api.waychaser.io/example/some-resource.
NOTE: The method property is not part of the specification for Link
(RFC8288) or Link-Template headers.
This means that if you use waychaser with a server that provides this headers and it uses the method property
for something else, then you're going to need a custom handler.
The following code demonstrates using waychaser with the REST API for AWS API Gateway to download the 'Error'
schema from 'test-waychaser' gateway
`js
import { waychaser, halHandler, MediaTypes } from '@mountainpass/waychaser'
import fetch from 'cross-fetch'
import aws4 from 'aws4'
// AWS makes us sign each request. This is a fetcher that does that automagically for us.
/**
* @param url
* @param options
*/
function awsFetch (url, options) {
const parsedUrl = new URL(url)
const signedOptions = aws4.sign(
Object.assign(
{
host: parsedUrl.host,
path: ${parsedUrl.pathname}?${parsedUrl.searchParams},
method: 'GET'
},
options
)
)
return fetch(url, signedOptions)
}
// Now we tell waychaser, to only accept HAL and to use our fetcher.
const awsWayChaser = waychaser.use(halHandler, MediaTypes.HAL).withFetch(awsFetch)
// now we can load the API
const api = await waychaser.load(
'https://apigateway.ap-southeast-2.amazonaws.com/restapis'
)
// then we can find the gateway we're after
const gateway = await api.ops
.filter('item')
.findInRelated({ name: 'test-waychaser' })
// then we can get the models
const models = await gateway.invoke(
'http://docs.aws.amazon.com/apigateway/latest/developerguide/restapi-restapi-models.html'
)
// then we can find the schema we're after
const model = await models.ops
.filter('item')
.findInRelated({ name: 'Error' })
// and now we get the schema
const schema = JSON.parse((await model.body()).schema)
`
NOTE: While the above is a legit, and it works (here's the test), for full
use of the AWS API Gateway REST API, you're going to need a custom handler.
This is because HAL links are supposed retrieved using a HTTP GET, but many of the AWS API Gateway REST API links
require using POST,
PATCH or
DELETE HTTP methods.
But there's nothing in AWS API Gateway links to tell you when to use a different HTTP method. Instead it's
communicated out-of-band in AWS API Gateway documentation. If you write a custom handler, please let me know 👍
While admittedly this is a toy example, the following code demonstrates using waychaser to complete the
Hypermedia in the Wizard's Tower text-based adventure game.
But even though it's a game, it shows how waychaser can easily navigate a complex process, including POSTing data andDELETEing resources.
`js`
return waychaser
.load('http://hyperwizard.azurewebsites.net/hywit/void')
.then(current =>
current.invoke('start-adventure', {
name: 'waychaser',
class: 'Burglar',
race: 'waychaser',
gender: 'Male'
})
)
.then(current => {
if (current.response.status <= 500) return current.invoke('related')
else throw new Error('Server Error')
})
.then(current => current.invoke('north'))
.then(current => current.invoke('pull-lever'))
.then(current =>
current.invoke({ rel: 'move', title: 'Cross the bridge.' })
)
.then(current => current.invoke('move'))
.then(current => current.invoke('look'))
.then(current => current.invoke('eat-snacks'))
.then(current => current.invoke('related'))
.then(current => current.invoke('north'))
.then(current => current.invoke('pull-lever'))
.then(current => current.invoke('look'))
.then(current => current.invoke('eat-snacks'))
.then(current => current.invoke('enter'))
.then(current => current.invoke('answer-skull', { master: 'Edsger' }))
.then(current => current.invoke('east'))
.then(current => current.invoke('smash-mirror-1') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-2') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-3') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-4') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-5') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-6') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('smash-mirror-7') || current)
.then(current => current.invoke('related') || current)
.then(current => current.invoke('look'))
.then(current => current.invoke('enter-mirror'))
.then(current => current.invoke('north'))
.then(current => current.invoke('down'))
.then(current => current.invoke('take-book-3'))
Loki is no longer use for storing operations and has been replaced with an subclass of Array. We originally
introduced Loki it's querying capability, but it turned out to be far to large a dependency.
Previously you could get the number of operations on a resource by calling
`js`
apiResource.count()
For 2.x, replace this with
`js`
apiResource.length
To find an operation, instead of using
`js`
apiResource.operations.findOne(relationship)
// or
apiResource.operations.findOne({ rel: relationship })
// or
apiResource.ops.findOne(relationship)
// or
apiResource.ops.findOne({ rel: relationship })
use
`js`
apiResource.operations.find(relationship)
// or
apiResource.operations.find({ rel: relationship })
// or
apiResource.operations.find(operation => {
return operation.rel === relationship
})
// or
apiResource.ops.find(relationship)
// or
apiResource.ops.find({ rel: relationship })
// or
apiResource.ops.find(operation => {
return operation.rel === relationship
})
Additionally when invoking an operation, you can use an array finder function as well. e.g. the following are all
equivalent
`js`
await apiResource.invoke(relationship)
await apiResource.invoke({ rel: relationship })
await apiResource.invoke(operation => {
return operation.rel === relationship
})
await apiResource.operations.invoke(relationship)
await apiResource.operations.invoke({ rel: relationship })
await apiResource.operations.invoke(operation => {
return operation.rel === relationship
})
await apiResource.ops.invoke(relationship)
await apiResource.ops.invoke({ rel: relationship })
await apiResource.ops.invoke(operation => {
return operation.rel === relationship
})
await apiResource.operations.find(relationship).invoke()
await apiResource.operations.find({ rel: relationship }).invoke()
await apiResource.operations.find(operation => {
return operation.rel === relationship
}).invoke()
await apiResource.ops.find(relationship).invoke()
await apiResource.ops.find({ rel: relationship }).invoke()
await apiResource.ops.find(operation => {
return operation.rel === relationship
}).invoke()
NOTE: When findOne could not find an operation, null was returned, whereas when find cannot find an operationundefined
it returns
waychaser now automatically provides an accept header in requests.
The accept header can be overridden for individual requests, by including an alternate header.accept value in theoptions
parameter when calling the invoke method.
The use method now expects both a handler and the mediaType it can handle. WayChaser uses the provided accept
mediaTypes to automatically generate the request header.
NOTE: Currently waychaser does use the corresponding content-type header to filter the responses passed touse
handlers. THIS MAY CHANGE IN THE FUTURE. Handlers should only process responses that match the mediaType provided
when they are registered using the method.
In 2.x waychaser would throw an Error if response.ok was false. This is no longer the case as some APIs provide
hypermedia responses for 4xx and 5xx responses.
Code like the following
`js`
try {
return apiResource.invoke(relationship)
} catch(error) {
if( error.response ) {
// handle error response...
}
else {
// handle fetch error
}
}
should be replaced with
`js`
try {
const resource = await apiResource.invoke(relationship)
if( resource.response.ok ) {
return resource
}
else {
// handle error response...
}
} catch(error) {
// handle fetch error
}
or if there is no special processing needed for error responses
`js`
try {
return apiResource.invoke(relationship)
} catch(error) {
// handle fetch error
}
In 2.x invoking an operation that didn't exist would throw an error, leading to code like
`js`
const found = apiResource.ops.find(relationship)
if( found ) {
return found.invoke()
}
else {
// handle op missing
}
In 3.x invoking an operation that doesn't exist returns undefined, allowing for simpler code, as follows
`js`
const resource = await apiResource.invoke(relationship)
if( resource === undefined ) {
// handle operation missing
}
or
`js`
return apiResource.invoke(relationship) || //... return a default
NOTE: When we say it returns undefined we actually mean undefined, NOT a promise the resolvesundefined
to . This is what makes the ...invoke(rel) || default code possible.
WayChaser 3.x now includes a location header hander, which will create an operation with the related relationship.location
This allows support for APIs that, when creating a resource (ie using POST), provide a to the created location
resource in the response, or APIs that, when updating a resource (ie using PUT or PATCH), provide a to the
updated resource in the response.
Previously WayChaser provided a default instance via the waychaser export. This is no longer the case and you willnew WayChaser()`
need to create your own instance using
Problems can be client side or server side
Client side
- fetch throws exception - No Response
- can't parse response - Has Response
- can parse response, but the type predicate fails - Has Response
Server side
- server returns problem document - Has Response
Response may include links that tell the client how to resolve,
so we want it to be a WayChaserResponse
Options:
1. invoke returns WayChaserResponse with problem or content
- client uses content !== undefined && problem === undefined to check if the were not problems
- unclear if we got a problem or not
2. invoke returns a clean WayChaserResponse with content or a WayChaserProblem with a problem document
- client would need to use instanceOf to differentiate
- clean WayChaserResponse has a response and content (which could be expectedly undefined)
- WayChaserProblem has a problem document and may or may not have a response
3. invoke returns a clean WayChaserResponse or a ProblemDocument with optional waychaser response as extention
- client would need to use instanceOf to differentiate
- if server returns PD, then do we wrap the PD? Feels ugly