In the previous chapter, we implemented a custom media type for Alexandria. In the World Wide Web, it’s usually better to reuse existing and proven solutions than baking your own.
In this chapter, we are going to review some of the options available to offer hypermedia representations to your clients. Some of them are already standards, while others are only specifications collecting feedback before taking the standard route (if they gather enough support).
Using standards or specifications instead of creating your own format has a number of advantages. First, since you’re using something used by many others, you will easily find client and server libraries to integrate those formats and do some processing for you.
This means less work for you and the developers who implement clients for your web APIs.
These standards, when combined with vocabularies like the one provided by the schema.org initiative, allows the creation of rich and extensible experiences.
I believe this is the path towards web machine learning and the creation of smarter clients that can understand the semantics of web APIs and websites. I believe everyone should start learning and experimenting, but I think we’re still a long ways off from a wide acceptance.
Using these standards might allow us, one day, to bridge the gap between machine and human users.
We are now going to take a look at a few hypermedia formats. Some of the following formats are only description formats (which means they are only used to explain the semantics of your representations), while others are complete solutions that embed semantic details directly.
Let’s start with the JSON-LD
format. JSON-LD
is a “lightweight Linked Data format,” endorsed by the W3C, that can be used to describe, link and organize data.
Data is messy and disconnected. JSON-LD organizes and connects it, creating a better Web.
— JSON-LD
The good thing with JSON-LD
is that it is used only to describe the semantics of your representations and can easily be integrated within any of your current applications without too much hassle. This will transform your representations from being meaningless JSON
documents to becoming self-descriptive by embedding their semantics.
JSON-LD is all about Linked Data, that’s why it’s part of the name (“LD”).
But what are they? Here again is a quote from the JSON-LD
website to address this question.
Linked Data empowers people that publish and use information on the Web. It is a way to create a network of standards-based, machine-readable data across websites. It allows an application to start at one piece of Linked Data, and follows embedded links to other pieces of Linked Data that are hosted on different sites across the Web.
— JSON-LD
Linked Data are a way for machines to understand and learn more about the web by adding discoverability and shared semantics.
Now let’s take a look at how documents are formatted with JSON-LD
. First, let’s say we have a default JSON
document for a book.
{
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
}
To explain the semantics of this representation (what each property means and contains) with JSON-LD
, we must first add a new property, called @context
.
{
"@context": {},
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
}
In this context property, we want to explain what this title
is. We can do that using a shared vocabulary like schema.org. On their website, we can find the Book entity that inherits from CreativeWork and Thing. Thing has a name
property that matches the title
of our book.
We can define that in the context:
{
"@context": {
"title": "http://schema.org/name",
"isbn": "https://schema.org/isbn"
},
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
}
Since we are using JSON-LD
, it’s better to follow the same naming conventions and use what the standard has to offer to define our semantics.
Let’s rename title
to name
.
{
"@context": {
"name": "http://schema.org/name",
"isbn": "https://schema.org/isbn"
},
"name": "Master Ruby Web APIs",
"isbn": "1234567890"
}
Next, we need to include a way to identify the current resource being retrieved by defining the @id
property. Each resource should have an identifier to retrieve their representations.
{
"@context": {
"name": "http://schema.org/name",
"isbn": "https://schema.org/isbn"
},
"@id": "http://alexandria.com/api/books/1",
"name": "Master Ruby Web APIs",
"isbn": "1234567890"
}
To add a related entity, like an author, we can simply add a new property and define it as being a @id
.
{
"@context": {
"name": "http://schema.org/name",
"isbn": "https://schema.org/isbn",
"author": { "@id": "https://schema.org/Person", "@type": "@id" }
},
"@id": "http://alexandria.com/api/books/1",
"name": "Master Ruby Web APIs",
"isbn": "1234567890",
"author": "http://alexandria.com/api/authors/1"
}
Finally, we can simplify this representation by using the Book
type and simply specifying that we are using the schema.org vocabulary.
{
"@context": {
"@vocab": "http://schema.org/",
"author": { "@type": "@id" }
},
"@id": "http://alexandria.com/api/books/1",
"@type": "http://schema.org/Book",
"name": "Master Ruby Web APIs",
"isbn": "1234567890",
"author": "http://alexandria.com/api/authors/1"
}
I hope this gave you an idea of what JSON-LD
can do and how to use it. For more information, here is the standard document.
Hydra relies on two technologies in order to simplify the development of hypermedia-driven web APIs:
JSON-LD has no way to tell clients what kind of operations can be performed on the resource. Hydra features an operation
property that can be used to do just that.
Let’s add it to our book representation to describe the POST
and DELETE
operations.
{
"@context": [
"http://www.w3.org/ns/hydra/core",
{
"@vocab": "http://schema.org/",
"author": { "@type": "@id" }
}
],
"@id": "http://alexandria.com/api/books/1",
"@type": "http://schema.org/Book",
"name": "Master Ruby Web APIs",
"isbn": "1234567890",
"author": "http://alexandria.com/api/authors/1",
"operation": [
{
"@type": "CreateResourceOperation",
"method": "POST",
"expects": {
"@id": "http://schema.org/Book",
"supportedProperty": [
{ "property": "title", "range": "Text" },
{ "property": "isbn", "range": "Text" },
{ "property": "author", "range": "URL" }
]
}
},
{
"@type": "DeleteResourceOperation",
"method": "DELETE"
}
]
}
You can read more about the Hydra Core Vocabulary, or play with the Hydra Console.
Collection+JSON
is a hypermedia type made to handle collections. It includes both the data format (what representations look like) and the semantics (what the data means).
Here is the minimal representation for a Collection+JSON
document.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books"
}
}
Usually, a Collection+JSON
representation will come with more properties: links
, items
, queries
and template
.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books",
"links" : [],
"items": [],
"queries" : [],
"template": {}
}
}
links
This property contains the related links for the resource. It can also be found at the item level.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books",
"links" : [
{ "rel" : "feed", "href" : "http://example.org/books/rss" }
],
"items": [
{
"data" : {},
"links": [
{
"rel" : "author",
"href" : "http://examples.org/authors/1",
"prompt" : "Author"
}
]
}
],
"queries" : [],
"template": {}
}
}
items
This is the actual list of items. Each of those items can have the href
, links
and data
properties.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books",
"links" : [],
"items" : [
{
"href" : "http://example.org/books/1",
"data" : [
{
"name" : "title",
"value" : "Master Ruby Web APIs",
"prompt" : "Book Title"
},
{
"name" : "isbn",
"value" : "1234567890",
"prompt" : "ISBN (10)"
}
],
"links" : [
{
"rel" : "author",
"href" : "http://examples.org/authors/1",
"prompt" : "Author"
}
]
},
{},
{}
],
"queries" : [],
"template" : {}
}
}
queries
The queries
property includes the various queries that can be made for this resource. For example, here is a representation with a filtering system similar to what we built in Alexandria.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books",
"items" : [
{},
{},
{}
],
"queries" : [
{
"rel" : "search",
"href" : "http://example.org/books?q",
"prompt" : "Search",
"data" : [
{"name" : "title_eq", "value" : ""},
{"name" : "title_cont", "value" : ""},
{"name" : "isbn_eq", "value" : ""}
]
}
],
"template" : {}
}
}
Including all the queries information makes it super easy for clients to reuse those values instead of guessing them.
template
The template you can define in this property is like a form in HTML. This lets the client knows that it can send POST
and PUT
requests using the given attributes to create or replace a book.
{ "collection" :
{
"version" : "1.0",
"href" : "http://example.org/books",
"links" : [],
"items" : [ {}, {}, {} ],
"queries" : [],
"template" : {
"data" : [
{"name" : "title", "value" : "", "prompt" : "Book Title"},
{"name" : "isbn", "value" : "", "prompt" : "ISBN (10)"},
{"name" : "author", "value" : "", "prompt" : "Author"}
]
}
}
}
Note that a single item is still represented as a collection, it’s just the only item in the collection.
You can read more about Collection+JSON
here. This gem can be used to build Collection+JSON
representations.
HAL is known as the Hypertext Application Language. Its goal is to give developers a simple and consistent way to link your resources with hyperlinks.
Adopting HAL will make your API explorable, and its documentation easily discoverable from within the API itself. In short, it will make your API easier to work with and therefore more attractive to client developers.
APIs that adopt HAL can be easily served and consumed using open source libraries available for most major programming languages. It’s also simple enough that you can just deal with it as you would any other JSON.
HAL is a lightweight media type that uses the concept of Resources and Links to model representations, either with JSON
or XML
.
Resources come with links, embedded resources and state. The state is simply the usual JSON
data for that resource.
{
"title": "Master Ruby Web APIs",
"price_cents": 299,
"price_currenty": "USD"
}
Links are identified with the _links
key, and should describe the relationship between the resource and the link. Minimally, a self
link should be included in the _links
property describing the URI of the current resource.
{
"_links": {
"self": { "href": "/books/1" }
},
"title": "Master Ruby Web APIs",
"price_cents": 299,
"price_currenty": "USD"
}
Let’s look at a more complex example. The representation below comes from the HAL documentation and is a collection of orders described with the hal+json
media type.
{
"_links": {
"self": { "href": "/orders" },
"curies": [
{
"name": "ea",
"href": "http://example.com/docs/rels/{rel}",
"templated": true
}
],
"next": { "href": "/orders?page=2" },
"ea:find": {
"href": "/orders{?id}",
"templated": true
},
"ea:admin": [{
"href": "/admins/2",
"title": "Fred"
}, {
"href": "/admins/5",
"title": "Kate"
}]
},
"currentlyProcessing": 14,
"shippedToday": 20,
"_embedded": {
"ea:order": [{
"_links": {
"self": { "href": "/orders/123" },
"ea:basket": { "href": "/baskets/98712" },
"ea:customer": { "href": "/customers/7809" }
},
"total": 30.00,
"currency": "USD",
"status": "shipped"
}, {
"_links": {
"self": { "href": "/orders/124" },
"ea:basket": { "href": "/baskets/97213" },
"ea:customer": { "href": "/customers/12369" }
},
"total": 20.00,
"currency": "USD",
"status": "processing"
}]
}
}
It includes hyperlinks and metadata in the _links
key. More specifically, it contains the current resource being represented, a next
link that contains the next page of order and a way to understand the semantics with the CURIEs templates. Those CURIEs allow a client to access a resource documentation.
currentlyProcessing
and shippedToday
are collection attributes for the list of orders.
Finally, the _embedded
key contains the list of orders with their own hyperlinks and related entities.
In Ruby, hal+json
representations can be generated using the roar gem, and the Internet Draft document is available here.
Siren is another hypermedia format - let’s take a look at how representations are formatted with this media type.
The fundamental element of Siren is the “Entity.”
An Entity is a URI-addressable resource that has properties and actions associated with it. It may contain sub-entities and navigational links.
Here is a book
entity - note that the class
property defines the nature of the content. We also use the properties
property to define the representation if the data of our book.
{
"class": [ "book" ],
"properties": {
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
},
"entities": [],
"actions": [],
"links": []
}
Entities can have sub-entities, and that’s exactly what the entities
property is for. We can define related entities like the author or the different editions of the book.
{
"class": [ "book" ],
"properties": {
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
},
"entities": [
{
"class": [ "editions", "collection" ],
"rel": [ "http://example.com/rels/editions" ],
"href": "http://api.example.com/books/2/editions"
},
{
"class": [ "author" ],
"rel": [ "http://example.com/rels/author" ],
"properties": {
"given_name": "Thibault",
"family_name": "Denizet"
},
"links": [
{ "rel": [ "self" ], "href": "http://api.example.com/authors/1" }
]
}
],
"actions": [],
"links": []
}
This property exposes the available behaviors for the entity. In the example below, we tell the client that a new edition can be created for the book by submitting a POST
request with the given fields.
{
"class": [ "book" ],
"properties": {
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
},
"entities": [...],
"actions": [
{
"name": "add-edition",
"title": "Add Edition",
"method": "POST",
"href": "http://api.example.com/books/2/editions",
"type": "application/x-www-form-urlencoded",
"fields": [
{ "name": "editionNumber", "type": "number" },
{ "name": "publisherName", "type": "text" }
]
}
],
"links": []
}
The links
property is pretty similar to what we have seen previously. With this property, we can define navigational links like some form of pagination.
{
"class": [ "book" ],
"properties": {
"title": "Master Ruby Web APIs",
"isbn": "1234567890"
},
"entities": [...],
"actions": [...],
"links": [
{ "rel": [ "self" ], "href": "http://api.example.com/books/2" },
{ "rel": [ "previous" ], "href": "http://api.example.com/books/1" },
{ "rel": [ "next" ], "href": "http://api.example.com/books/3" }
]
}
You can read more about Siren here.
The last format we are going to talk about is JSON-API.
I wrote a series of articles showing how to create an implementation of this specification from scratch, if you’re interested.
Here is the minimal representation we will build upon while learning more about the JSON-API
specification.
{
"meta": {},
"links": {},
"data": [],
"included": []
}
meta
The meta
property can be used to include meta-information about the API like its authors, some copyright or licensing, etc. This is an optional property.
{
"meta": {
"copyright": "Copyright 2016 Alexandria Inc.",
"authors": ["John Doe"]
},
"links": {},
"data": [],
"included": []
}
links
The links
property can be used to represent related links for the current resource.
{
"links": {
"self": "http://example.com/books"
},
"data": [],
"included": []
}
data
The data
property is where we include the actual properties of the resource. In the example below, we have a list of books containing only one book, and two related resources: an author and a list of editions.
{
"links": {
"self": "http://example.com/books"
},
"data": [{
"type": "books",
"id": "1",
"attributes": {
"title": "Master Ruby Web APIs"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/books/1/relationships/author",
"related": "http://example.com/books/1/author"
},
"data": { "type": "authors", "id": "1" }
},
"editions": {
"links": {
"self": "http://example.com/books/1/relationships/editions",
"related": "http://example.com/books/1/editions"
},
"data": [
{ "type": "editions", "id": "3" },
{ "type": "editions", "id": "42" }
]
}
},
"links": {
"self": "http://example.com/books/1"
}
}],
"included": []
}
Note that we don’t have any property for the author or the editions; only links, types and identifiers. That’s intentional, and the client can request those related resources if needed.
included
There is a way, however, to create compound documents in which related entities are included in the same representation - this is what the included
property is for. It is a root-level property to reduce the representation side by only including the same entity once, even if it is related to more than one entity.
In the example below, we have two books that were written by the same author. We can include that author only once in the include
property.
{
"links": {
"self": "http://example.com/books"
},
"data": [{
"type": "books",
"id": "1",
"attributes": {
"title": "Master Ruby Web APIs"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/books/1/relationships/author",
"related": "http://example.com/books/1/author"
},
"data": { "type": "authors", "id": "1" }
}
},
"links": {
"self": "http://example.com/books/1"
}
},{
"type": "books",
"id": "2",
"attributes": {
"title": "Modular Rails"
},
"relationships": {
"author": {
"links": {
"self": "http://example.com/books/2/relationships/author",
"related": "http://example.com/books2/author"
},
"data": { "type": "authors", "id": "1" }
}
},
"links": {
"self": "http://example.com/books/2"
}
}
],
"included": [{
"type": "authors",
"id": "1",
"attributes": {
"given-name": "Thibault",
"family-name": "Denizet"
},
"links": {
"self": "http://example.com/authors/1"
}
}]
}
The JSON-API
specification is pretty opinionated on how interactions should happen between the client and the server. This is great, because it makes it easier to create shared clients for server supporting the JSON-API
specification. For example, the specification specifies how sorting, pagination or filtering should happen.
Additionally, it includes how resources can be created, updated or deleted.
For example, the POST
request below can be used to create a new book, but should be formatted exactly like this, as defined by the JSON-API
specification.
POST /books HTTP/1.1
Content-Type: application/vnd.api+json
Accept: application/vnd.api+json
{
"data": {
"type": "books",
"attributes": {
"title": "Master Ruby Web APIs",
"cover": "http://example.com/images/cover.png"
},
"relationships": {
"author": {
"data": { "type": "authors", "id": "1" }
}
}
}
}
If you’d like to use this specification with Ruby, check out Active Model Serializers that will come with default support in the next release.
You can learn more about the JSON-API
specification here. I really recommend reading through the specification, as I think it’s a well thought hypermedia format and you might pick up a few things from there.
I can’t really tell you which format you should use. Since none of them have been accepted as the “best hypermedia format,” it’s totally up to you. I would still recommend using one of those if you’re building a hypermedia API, as this will simplify the API development by guiding you and helping you defining your representations.
So, which one will you use?