Let’s explore Siren’s H-Factors, a set of hypermedia features measuring the sophistication of a hypertext format. In doing so, we’ll see how Siren’s hypertext elements are translated to HTTP requests.

I assume you have a basic understanding of hypertext, as well as Siren and it’s elements. If you’re new to Siren, I recommend reading the specification.

In Siren, link support is achieved through the use of links, embedded links, and actions. These elements communicate different types of HTTP requests available to a client from a given entity.

Link objects act as outbound links. To follow a link, a client makes an HTTP GET request using the href as the request target. For example, suppose we have the following link:

{
  "rel": ["https://schema.org/author"],
  "href": "https://api.example.com/author"
}

A client would follow the link by generating this request:

GET https://api.example.com/author HTTP/1.1

When the href is relative, consider resolving the reference as outlined here.

Because they are navigational in nature, links can point to any related resource. Therefore it’s not safe to assume the response to following a link includes Siren content.

Embedded links are supported via the aptly named embedded link object, which “communicate a relationship between entities” [Siren]. Unlike link objects, embedded links only point to other entities. Therefore we can’t embed other documents (images, PDFs, etc.) in an entity like we can in other hypertext formats such as HTML.

A client can resolve an embedded link to an embedded entity by making an HTTP GET request using the href as the target. The embedded link in the context entity is then replaced with the response content plus the rel property from the embedded link.

As an example, consider the following entity located at https://api.example.com/books/the-way-of-zen:

{
  "class": ["Book"],
  "entities": [{ "rel": ["author"], "href": "/people/alan-watts" }],
  "links": [{ "rel": ["self"], "href": "/books/the-way-of-zen" }]
}

To resolve the author sub-entity, a client makes the following request:

GET /people/alan-watts HTTP/1.1
Host: api.example.com
HTTP/1.1 200 OK
Content-Type: application/vnd.siren+json

{
  "class": ["Person"],
  "links": [
    { "rel": ["self"], "href": "/people/alan-watts" }
  ]
}

The client then adds the rel property to the content and replaces the embedded link with the result.

{
  "class": ["Book"],
  "entities": [
    {
      "rel": ["author"],
      "class": ["Person"],
      "links": [{ "rel": ["self"], "href": "/people/alan-watts" }]
    }
  ],
  "links": [{ "rel": ["self"], "href": "/books/the-way-of-zen" }]
}

Templated Queries

An action whose method is GET represents a templated query. A client submits this type of action by making an HTTP GET request, using the href as the request target. The fields are serialized as a URL-encoded form where the name-value pairs are the name and value of each field. The result is used as the request URI’s query.

{
  "title": "Search Orders",
  "name": "search",
  "href": "/orders",
  "fields": [{ "name": "orderNumber" }]
}

Given the search action above, suppose a value of foo is provided for the orderNumber field. Submitting the action generates this HTTP request:

GET /orders?orderNumber=foo HTTP/1.1
Host: api.example.com

As with outbound links, it is not safe to assume the response to submitting an action will contain Siren content.

Non-Idempotent Updates

Actions with a non-idempotent HTTP method (e.g., POST, PATCH) for the method property are used for non-idempotent updates.

{
  "title": "Add Item",
  "name": "add-item",
  "method": "POST",
  "href": "/orders/42/items",
  "fields": [
    { "name": "orderNumber", "type": "hidden", "value": "42" },
    { "name": "productCode", "type": "text" },
    { "name": "quantity", "type": "number", "class": ["integer"] }
  ]
}

Given the values ABC123 and 10 for the productCode and quantity fields above, submitting the add-item action generates this request:

POST /orders/42/items HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 45

orderNumber=42&productCode=ABC123&quantity=10

Idempotent Updates

Lastly in link support, idempotent updates are represented by actions with an idempotent method (e.g., PUT, DELETE). If the HTTP method doesn’t support request content (i.e., DELETE), fields are serialized as a URL-encoded form into the request URI’s query, similar to a templated query. Otherwise, fields are serialized into the request body.

{
  "title": "Remove Order",
  "name": "remove",
  "method": "DELETE",
  "href": "/orders/42",
  "fields": [{ "name": "archive", "type": "checkbox" }]
}
DELETE /orders/42?archive=false HTTP/1.1
Host: api.example.com

Control Data

Certain properties of the link-supporting objects provide support for control data.

The link’s and embedded link’s rel property allows specifying the relation type to the target resource.

Control Data for Interface Methods

The action’s method property specifies the HTTP method to use when submitting an action, which is what allows Siren to support Templated Queries, Non-Idempotent Updates, and Idempotent Updates.

Control Data for Read Requests

The link’s type property “defines [the] media type of the linked resource, per Web Linking (RFC5988)”. However, according to RFC 8288, which obsoletes RFC 5988, “the type attribute, when present, is only a hint; for example, it does not override the Content-Type header field of a HTTP response obtained by actually following the link.” Therefore the type attribute has no effect on HTTP messages.

Since embedded links only point to other entities, their type property is irrelevant.

Control Data for Update Requests

Finally, the action’s type property indicates to clients how the action’s fields are to be serialized. We saw how to serialize application/x-www-form-urlencoded actions above. Now let’s see how we might serialize a multipart/form-data action. Consider the following add-invoice action:

{
  "title": "Upload Invoice",
  "name": "add-invoice",
  "method": "PUT",
  "href": "/orders/42/invoice",
  "type": "multipart/form-data",
  "fields": [
    { "name": "orderNumber", "type": "hidden", "value": "42" },
    { "name": "invoice", "type": "file" }
  ]
}

Following the rules outlined in RFC 7578, a client would generate the following request when submitting the action, assuming a file named invoice.pdf is provided for the invoice field:

PUT /orders/42/invoice HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=foobar

--foobar
Content-Disposition: form-data; name="orderNumber"

42
--foobar
Content-Disposition: form-data; name="invoice"; filename="invoice.pdf"
Content-Type: application/pdf
Content-Length: 69420

...file bytes...
--foobar--

Summary

Siren supports nearly every H-factor. Erring on the side of caution, the only factor not supported is control data for read requests.

Siren H-Factors

Compared to other hypertext formats’ H-factors, like HAL or Collection+JSON, Siren’s hypermedia support is quite robust. With Siren we can provide nearly every protocol detail a web API needs at runtime.