10.1.10 Hypermedia as the Engine of Application State - Reference Documentation
Authors: Graeme Rocher, Peter Ledbrook, Marc Palmer, Jeff Brown, Luke Daley, Burt Beckwith, Lari Hotari
Version: 3.1.9
10.1.10 Hypermedia as the Engine of Application State
HATEOAS, an abbreviation for Hypermedia as the Engine of Application State, is a common pattern applied to REST architectures that uses hypermedia and linking to define the REST API.Hypermedia (also called Mime or Media Types) are used to describe the state of a REST resource, and links tell clients how to transition to the next state. The format of the response is typically JSON or XML, although standard formats such as Atom and/or HAL are frequently used.10.1.10.1 HAL Support
HAL is a standard exchange format commonly used when developing REST APIs that follow HATEOAS principals. An example HAL document representing a list of orders can be seen below:{ "_links": { "self": { "href": "/orders" }, "next": { "href": "/orders?page=2" }, "find": { "href": "/orders{?id}", "templated": true }, "admin": [{ "href": "/admins/2", "title": "Fred" }, { "href": "/admins/5", "title": "Kate" }] }, "currentlyProcessing": 14, "shippedToday": 20, "_embedded": { "order": [{ "_links": { "self": { "href": "/orders/123" }, "basket": { "href": "/baskets/98712" }, "customer": { "href": "/customers/7809" } }, "total": 30.00, "currency": "USD", "status": "shipped" }, { "_links": { "self": { "href": "/orders/124" }, "basket": { "href": "/baskets/97213" }, "customer": { "href": "/customers/12369" } }, "total": 20.00, "currency": "USD", "status": "processing" }] } }
Exposing Resources Using HAL
To return HAL instead of regular JSON for a resource you can simply override the renderer ingrails-app/conf/spring/resources.groovy
with an instance of grails.rest.render.hal.HalJsonRenderer
(or HalXmlRenderer
for the XML variation):import grails.rest.render.hal.* beans = { halBookRenderer(HalJsonRenderer, rest.test.Book) }
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books/1HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/hal+json;charset=ISO-8859-1{ "_links": { "self": { "href": "http://localhost:8080/books/1", "hreflang": "en", "type": "application/hal+json" } }, "title": ""The Stand"" }
import grails.rest.render.hal.* beans = { halBookRenderer(HalXmlRenderer, rest.test.Book) }
Rendering Collections Using HAL
To return HAL instead of regular JSON for a list of resources you can simply override the renderer ingrails-app/conf/spring/resources.groovy
with an instance of grails.rest.render.hal.HalJsonCollectionRenderer
:import grails.rest.render.hal.* beans = { halBookCollectionRenderer(HalJsonCollectionRenderer, rest.test.Book) }
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/hal+json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 17 Oct 2013 02:34:14 GMT{ "_links": { "self": { "href": "http://localhost:8080/books", "hreflang": "en", "type": "application/hal+json" } }, "_embedded": { "book": [ { "_links": { "self": { "href": "http://localhost:8080/books/1", "hreflang": "en", "type": "application/hal+json" } }, "title": "The Stand" }, { "_links": { "self": { "href": "http://localhost:8080/books/2", "hreflang": "en", "type": "application/hal+json" } }, "title": "Infinite Jest" }, { "_links": { "self": { "href": "http://localhost:8080/books/3", "hreflang": "en", "type": "application/hal+json" } }, "title": "Walden" } ] } }
Book
objects in the rendered JSON is book
which is derived from the type of objects in the collection, namely Book
. In order to customize the value of this key assign a value to the collectionName
property on the HalJsonCollectionRenderer
bean as shown below:import grails.rest.render.hal.* beans = { halBookCollectionRenderer(HalCollectionJsonRenderer, rest.test.Book) { collectionName = 'publications' } }
$ curl -i -H "Accept: application/hal+json" http://localhost:8080/books HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/hal+json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 17 Oct 2013 02:34:14 GMT{ "_links": { "self": { "href": "http://localhost:8080/books", "hreflang": "en", "type": "application/hal+json" } }, "_embedded": { "publications": [ { "_links": { "self": { "href": "http://localhost:8080/books/1", "hreflang": "en", "type": "application/hal+json" } }, "title": "The Stand" }, { "_links": { "self": { "href": "http://localhost:8080/books/2", "hreflang": "en", "type": "application/hal+json" } }, "title": "Infinite Jest" }, { "_links": { "self": { "href": "http://localhost:8080/books/3", "hreflang": "en", "type": "application/hal+json" } }, "title": "Walden" } ] } }
Using Custom Media / Mime Types
If you wish to use a custom Mime Type then you first need to declare the Mime Types ingrails-app/conf/application.groovy
:grails.mime.types = [ all: "*/*", book: "application/vnd.books.org.book+json", bookList: "application/vnd.books.org.booklist+json", … ]
It is critical that place your new mime types after the 'all' Mime Type because if the Content Type of the request cannot be established then the first entry in the map is used for the response. If you have your new Mime Type at the top then Grails will always try and send back your new Mime Type if the requested Mime Type cannot be established.Then override the renderer to return HAL using the custom Mime Types:
import grails.rest.render.hal.* import grails.web.mime.*beans = { halBookRenderer(HalJsonRenderer, rest.test.Book, new MimeType("application/vnd.books.org.book+json", [v:"1.0"])) halBookListRenderer(HalJsonCollectionRenderer, rest.test.Book, new MimeType("application/vnd.books.org.booklist+json", [v:"1.0"])) }
application/vnd.books.org.book+json
. The second bean defines the Mime Type used to render a collection of books (in this case application/vnd.books.org.booklist+json
).
application/vnd.books.org.booklist+json
is an example of a media-range (http://www.w3.org/Protocols/rfc2616/rfc2616.html - Header Field Definitions). This example uses entity (book) and operation (list) to form the media-range values but in reality, it may not be necessary to create a separate Mime type for each operation. Further, it may not be necessary to create Mime types at the entity level. See the section on "Versioning REST resources" for further information about how to define your own Mime types.
With this in place issuing a request for the new Mime Type returns the necessary HAL:$ curl -i -H "Accept: application/vnd.books.org.book+json" http://localhost:8080/books/1HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/vnd.books.org.book+json;charset=ISO-8859-1 { "_links": { "self": { "href": "http://localhost:8080/books/1", "hreflang": "en", "type": "application/vnd.books.org.book+json" } }, "title": ""The Stand"" }
Customizing Link Rendering
An important aspect of HATEOAS is the usage of links that describe the transitions the client can use to interact with the REST API. By default theHalJsonRenderer
will automatically create links for you for associations and to the resource itself (using the "self" relationship).However you can customize link rendering using the link
method that is added to all domain classes annotated with grails.rest.Resource
or any class annotated with grails.rest.Linkable
. For example, the show
action can be modified as follows to provide a new link in the resulting output:def show(Book book) { book.link rel:'publisher', href: g.createLink(absolute: true, resource:"publisher", params:[bookId: book.id]) respond book }
{ "_links": { "self": { "href": "http://localhost:8080/books/1", "hreflang": "en", "type": "application/vnd.books.org.book+json" } "publisher": { "href": "http://localhost:8080/books/1/publisher", "hreflang": "en" } }, "title": ""The Stand"" }
link
method can be passed named arguments that match the properties of the grails.rest.Link
class.
10.1.10.2 Atom Support
Atom is another standard interchange format used to implement REST APIs. An example of Atom output can be seen below:<?xml version="1.0" encoding="utf-8"?> <feed xmlns="http://www.w3.org/2005/Atom"> <title>Example Feed</title> <link href="http://example.org/"/> <updated>2003-12-13T18:30:02Z</updated> <author> <name>John Doe</name> </author> <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id> <entry> <title>Atom-Powered Robots Run Amok</title> <link href="http://example.org/2003/12/13/atom03"/> <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id> <updated>2003-12-13T18:30:02Z</updated> <summary>Some text.</summary> </entry></feed>
import grails.rest.render.atom.* beans = { halBookRenderer(AtomRenderer, rest.test.Book) halBookListRenderer(AtomCollectionRenderer, rest.test.Book) }
10.1.10.3 Vnd.Error Support
Vnd.Error is a standardised way of expressing an error response.By default when a validation error occurs when attempting to POST new resources then the errors object will be sent back allow with a 422 respond code:$ curl -i -H "Accept: application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/booksHTTP/1.1 422 Unprocessable Entity Server: Apache-Coyote/1.1 Content-Type: application/json;charset=ISO-8859-1{"errors":[{"object":"rest.test.Book", "field":"title", "rejected-value":null, "message":"Property [title] of class [class rest.test.Book] cannot be null"}]}
grails.rest.render.errors.VndErrorJsonRenderer
bean in grails-app/conf/spring/resources.groovy
:
beans = { vndJsonErrorRenderer(grails.rest.render.errors.VndErrorJsonRenderer) // for Vnd.Error XML format vndXmlErrorRenderer(grails.rest.render.errors.VndErrorXmlRenderer) }
$ curl -i -H "Accept: application/vnd.error+json,application/json" -H "Content-Type: application/json" -X POST -d "" http://localhost:8080/books HTTP/1.1 200 OK Server: Apache-Coyote/1.1 Content-Type: application/vnd.error+json;charset=ISO-8859-1[ { "logref": ""book.nullable"", "message": "Property [title] of class [class rest.test.Book] cannot be null", "_links": { "resource": { "href": "http://localhost:8080/rest-test/books" } } } ]