Using HAL with JSON Views
Learn to build discoverable APIs with Grails
Authors: ZacharyKlein
Grails Version: 3.3.1
1 Grails Training
Grails Training - Developed and delivered by the folks who created and actively maintain the Grails framework!.
2 Getting Started
In this guide you are going to explore HAL (Hypertext Application Language) support in Grails, using JSON views (provided by the Grails Views library).
Hypertext Application Language (HAL) is an Internet Draft (a "work in progress") standard convention for defining hypermedia such as links to external resources within JSON or XML code.
The purpose of HAL is make APIs "discoverable" - it defines a set of conventions that allow consumers of your API to follow links between resources, as well as providing pagination and other convenience features for "exploring" an API. HAL is a popular standard for implementing HATEOAS (Hypermedia As The Engine Of Application State) architecture, which is an extension of basic REST architecture.
For a thorough introduction to HAL, check out the overview and specification at the following URL: http://stateless.co/hal_specification.html |
Grails provides support for HAL through JSON views, which are part of the Grails Views library. You can use this library in an existing application following the installation steps in the documentation, or you can create a new application using either the rest-api
profile or one of the frontend profiles (angular
, angular2
and react
) which extend the rest-api
profile.
In this guide, we have provided a basic Grails 3.3.1 application using the rest-api
profile in the initial
project. We have also included a few domain classes to expose via our API. You may generate your own project if you wish (in which case you will need to copy the domain classes from initial/grails-app/domain/
into your own project), or simply use the initial
project to follow along with the guide.
2.1 What You will Need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 1.7 or greater installed with
JAVA_HOME
configured appropriately
2.2 How to Complete the Guide
To get started do the following:
-
Download and unzip the source
or
-
Clone the Git repository:
git clone https://github.com/grails-guides/using-hal-with-json-views.git
The Grails guides repositories contain two folders:
-
initial
Initial project. Often a simple Grails app with some additional code to give you a head-start. -
complete
A completed example. It is the result of working through the steps presented by the guide and applying those changes to theinitial
folder.
To complete the guide, go to the initial
folder
-
cd
intograils-guides/using-hal-with-json-views/initial
and follow the instructions in the next sections.
You can go right to the completed example if you cd into grails-guides/using-hal-with-json-views/complete
|
3 Running the Application
Grails provides excellent support for RESTful url mappings to expose domain resources. In the initial
project, we’ve already annotated our domain classes with the @Resource
annotation, which generates a RestfulController
and associated URL mappings to expose each domain class as a restful resource.
package com.example
import grails.rest.*
@Resource(readOnly = true, uri='/api/customers') (1)
class Customer {
1 | The @Resource annotation takes several optional arguments, including a URL endpoint and format options, as well as whether the API should only expose read endpoints |
Adding these annotations to our domain classes gives us a running start to creating our API. See the Grails documentation for more information on @Resource
To run the application use the ./gradlew bootRun
command which will start the application on port 8080.
Now that the Grails app is running, we can try out the API that Grails generated for us via the @Resource
annotation. Make a GET request to /api/products
to retrieve a list of products:
curl -H "Accept: application/json" localhost:8080/api/products
[
{
"id": 1,
"category": {
"id": 1
},
"inventoryId": "CLOTH001",
"name": "Cargo Pants",
"price": 15.00
},
{
"id": 2,
"category": {
"id": 1
},
"inventoryId": "CLOTH002",
"name": "Sweater",
"price": 12.00
},
{
"id": 3,
"category": {
"id": 1
//...
]
Here, a GET request to /api/orders/1
returns the Order
with an id of 1:
curl -H "Accept: application/json" localhost:8080/api/orders/1
{
"id" : 1,
"shippingAddress" : {
"id" : 1
},
"shippingCost" : 13.54,
"orderId" : "0A12321",
"orderPlaced" : "2017-02-08T09:10:36Z",
"products" : [
{
"id" : 11
},
{
"id" : 6
},
{
"id" : 1
}
],
"customer" : {
"id" : 1
}
}
Because we’ve specified readOnly = true in our @Resource annotations, Grails will not generate endpoints for update/create/delete operations. This is sufficient for the steps in this guide, but you can remove the readOnly property (or set it to false ) to enable the "write" operations.
|
4 Building our API
The default JSON rendered by Grails is a good start, but it doesn’t necessarily express the details we want in our public-facing API. JSON Views allow us to render our data using Groovy’s StreamingJsonBuilder
, in a statically-compiled Groovy view. JSON views provide a powerful DSL-based tool for expressing the JSON output from our API.
4.1 Introducing JSON Views
Here’s a trivial example of a JSON view:
json.message {
hello "world"
}
This JSON view would produce the following ouput when rendered:
{"message":{ "hello":"world"}}
JSON views also support a model
which references the data passed in to the view, as seen below:
model {
String message
}
json.message {
hello message
}
JSON views are Groovy files with the file extension gson
, and they reside in the grails-app/views
directory, just like GSP views. They are resolved (by convention) to a controller with the same name as the view directory, again like GSP views.
4.2 Customizing our API
Let’s create a JSON view to customize the output from the /api/orders/$id
endpoint. Right now, the default JSON renderer includes ids of all associated objects. However, we don’t want to expose the id
of the shippingAddress
property (which is an instance of our Address
domain class) - it’s not exposed as a domain resource in our API and it’s only relevant to users of API as part of an Order
or Customer
. Ideally, we’d like to include the shippingAddress
fields in the JSON output of our Order
API.
In addition, we’d like to express the orderId
property as the order’s id
, rather than the actual id from the database.
Create a new directory under grails-app/views
, called order
:
$ mkdir grails-app/views/order
If you’re familiar with Grails' view resolution, you may be thinking we need to create an OrderController in order to use views from our order directory. We could do that, however because we’re making use of the @Resource annotation on our domain class, Grails will generate an associated OrderController (which will in turn inherit from RestfulController ) for us. So at this point, we don’t need to create a controller for our Order class.
|
Create a new JSON view called show.gson
. This will resolve to the show
action in our controller, just as a show.gsp
page would in a normal Grails application. Edit the new view with the following content:
import com.example.Order
model {
Order order
}
json {
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy') (1)
shippingAddress { (2)
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products order.products.collect { [id: it.id] } (3)
customer {
id order.customer.id
}
}
1 | Note that we are using Groovy’s format method on the Date class to customize the format of the orderPlaced property |
2 | Here we’re filling out our shippingAddress with the fields from the Address class |
3 | Notice that we’re iterating over a collect (order.products ) with the collect method and returning a map - this will create a JSON array of objects |
Now if you make a request to /api/orders/1
, you should see the following output:
curl -H "Accept: application/json" localhost:8080/api/orders/1
{
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
id: 11
},
{
id: 1
},
{
id: 6
}
],
customer: {
id: 1
}
}
Let’s create another JSON view for our Customer
domain class. Create a new directory under grails-app/views
, called customer
, and a new JSON view show.gson
$ mkdir grails-app/views/customer
Create a new JSON view called show.gson
. This will resolve to the show
action in our controller, just as a show.gsp
page would in a normal Grails application. Edit the new view with the following content:
import com.example.Customer
model {
Customer customer
}
json {
id customer.id
firstName customer.firstName
lastName customer.lastName
fullName "${customer.firstName} ${customer.lastName}"
address {
street customer.address.street
street2 customer.address.street2
city customer.address.city
state customer.address.state
zip customer.address.zip
}
orders customer.orders.collect { [id: it.id] }
}
5 Make our API Discoverable via HAL
We’ve customized our API somewhat now, but we have a few issues:
-
By customizing our
id
property for orders, it’s no longer clear to clients how to reference a particulate record (since our API still relies on the database id) -
Our three exposed domain classes have a number of associations where only ids are exposed - this means that clients need to make a new request to get the details on an associated object (e.g, a client consuming an order would need to make separate requests to obtain the fields for its products)
-
Our API is rather opaque - without documentation, users of our API would have to guess the endpoints to arrive at associated records. Even with documentation, clients would likely need to use custom code to navigate our API, without a consistent standard to follow.
The conventions of the HAL+JSON standard can help us solve these issues, and JSON views provide first-class support for HAL - let’s see how we can use it.
5.1 Linked Resources
Links are the key in the HAL standard. HAL resources include a special field called _links
, which contains an array of JSON objects that define links to related resources. A HAL link contains (at least) two pieces of information - a relationship, and an href
containing the URL to access the related resource. Other metadata can be included as well.
Here’s a sample of a JSON body with a _links
field, and two links:
{
title: "Groovy Recipes",
author: "Scott Davis",
pages: 100,
"_links": {
"self": {
"href": "http://localhost:8080/book/show/1", (1)
},
"author": {
"href": "http://localhost:8080/author/show/1", (2)
}
}
}
1 | self is a special link that every HAL resource should include - it specifies the URL "back" to the current resource. |
2 | Here we’re defining a custom link called author that specifies the URL for the author field |
When a HAL resource is accessed by a client, it is possible to "browse" the relationships expressed in the _links
field without the client knowing the exact makeup of the API endpoint.
//examples are using the fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
//retrieve a book instance from the API
fetch("http://localhost:8080/api/book/1").then(function(response) {
return response.json();
}).then(function(data) {
this.book = data;
});
//retrieve book's author using links
var author;
fetch(book._links.author.href).then(function(response) {
return response.json();
}).then(function(data) {
author = data;
});
JSON views implement a Groovy trait HalView
, which exposes a hal
helper with several methods for outputting HAL-compliant JSON. One of those is the links
method:
model {
Order order
}
json {
hal.links(order)
//...
}
Calling hal.links()
on our domain resource produces the following JSON output:
{
_links: {
self: {
href: "http://localhost:8080/api/orders/1",
hreflang: "en_US",
type: "application/hal+json"
}
}
}
Let’s edit our order/show.gson
view to include a self
link as well as a link to the associated customer:
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer) (1)
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy')
shippingAddress {
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products order.products.collect { [id: it.id] }
customer {
id order.customer.id
}
}
1 | The links method can take a domain resource instance, or a map of link names and objects to link. |
Make a request to http://localhost:8080/api/orders/1
:
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/orders?id=1",
hreflang: "en",
type: "application/hal+json"
},
customer: {
href: "http://localhost:${serverPort}/api/customers?id=1",
hreflang: "en",
type: "application/hal+json"
}
},
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
id: 11
},
{
id: 1
},
{
id: 6
}
],
customer: {
id: 1
}
}
//...
}
5.2 Render Collections with Templates
Order
includes a one-many relationship with Product
, and right now our API returns a simple list of ids to represent products in an order. Ideally we’d like to include links to these products as well, so that clients of our API can retrieve the details of each product by following the links we provide. We can use JSON views' template functionality.
In the grails-app/views/order
directory, create a new JSON template with the name _product.gson
:
import com.example.Product
model {
Product product
}
json {
hal.links(product)
id product.id
}
Now in our order/show.gson
view, we can pass order.products
to the tmpl
helper, using our new _product
template:
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer)
id order.orderId
shippingCost order.shippingCost
date order.orderPlaced.format('M-dd-yyy')
shippingAddress {
street order.shippingAddress.street
street2 order.shippingAddress.street2
city order.shippingAddress.city
state order.shippingAddress.state
zip order.shippingAddress.zip
}
products tmpl.product(order.products) (1)
}
1 | The tmpl helper will resolve a method name to a template in the current view directory - e.g, tmpl.product will resolve to /order/_product.gson . If you want to access a template from outside the current directory, you can use an absolute path (relative to the views directory) as a string: tmpl."/customer/order"() will resolve to grails-app/views/customer/_order.gson . |
For more information on using templates in JSON views, see the Grails Views documentation. |
Make a request to http://localhost:8080/api/orders/1
, and you should see _links
for each product in the products
array:
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/orders?id=1",
hreflang: "en",
type: "application/hal+json"
},
customer: {
href: "http://localhost:${serverPort}/api/customers?id=1",
hreflang: "en",
type: "application/hal+json"
}
},
id: "0A12321",
shippingCost: 13.54,
date: "2-08-2017",
shippingAddress: {
street: "321 Arrow Ln",
street2: null,
city: "Chicago",
state: "IL",
zip: 646465
},
products: [
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/11",
hreflang: "en",
type: "application/hal+json"
}
},
id: 11
},
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/6",
hreflang: "en",
type: "application/hal+json"
}
},
id: 6
},
{
_links: {
self: {
href: "http://localhost:${serverPort}/api/products/1",
hreflang: "en",
type: "application/hal+json"
}
},
id: 1
}
]
}
Let’s use the same technique for the Customer’s orders - create a new template at `grails-app/views/customer/_order.gson
:
import com.example.Order
model {
Order order
}
json {
hal.links(order)
id order.id
}
Edit the customer/show.gson
view to use the new _order
template:
Let’s use the same technique for the Customer’s orders - create a new template at `grails-app/views/customer/_order.gson
:
import com.example.Customer
model {
Customer customer
}
json {
id customer.id
firstName customer.firstName
lastName customer.lastName
fullName "${customer.firstName} ${customer.lastName}"
address {
street customer.address.street
street2 customer.address.street2
city customer.address.city
state customer.address.state
zip customer.address.zip
}
orders tmpl.order(customer.orders)
}
5.3 Embed Associated Objects
Let’s take a look at our Product
domain resource. Product
has a belongsTo
relationship with Category
, which is expressed in our default JSON output in a simple object with the category id:
{
id: 1,
category: {
id: 1
},
inventoryId: "CLOTH001",
name: "Cargo Pants",
price: 15
}
Again we’d like to make it easier for consumers of the API to obtain the category for the product. We have several options:
-
We could simply include the
category
details in our JSON view: This approach obscures the boundary between theProduct
andCategory
resources in our API - it gives the (incorrect) impression to the client thatcategory.name
is a property ofProduct
, rather than a API resource in its own right. -
We could provide a link to the category: This approach would require clients to make a new request to get the category details, and it’s likely that most clients will want both the product and the category details in the same request.
You may recall that in the case of an order’s shippingAddress , we used the first of these two approaches (including the associated object’s details in the JSON view) - this is because Address is not exposed in our API as a resource, so an Address is effectively part of either an Order (shippingAddress ) or a Customer (address ) as far as our API is concerned.
|
HAL specifies an _embedded
property to represent cross-resource relationships in a nested format. With an embedded approach, we could include the Category
in the same HAL+JSON response, but the category would be in a separate element to make it clear that these are separate resources.
JSON views provide an embedded
method (via the hal
helper) that will generate an _embedded
element in our JSON view. It will include the default JSON output for the embedded objects as well as a _self
link for each object. Let’s use this to embed the category in our product output.
Create a new directory under grails-app/views
, called product
:
$ mkdir grails-app/views/product/
Create a new JSON view under this directory, called show.gson
:
import com.example.Product
model {
Product product
}
json {
hal.links(product)
hal.embedded(category: product.category) (1)
id product.inventoryId
name product.name
price product.price
}
1 | We’re passing embedded method a map of element names (in this case category ) to objects to embed (product.category ) |
Now make a request to http://localhost:8080/api/products/1
:
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"_embedded": {
"category": {
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/categories/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Clothing",
"version": 0
}
},
"id": "CLOTH001",
"name": "Cargo Pants",
"price": 15.00
}
In previous code snippet, the category
object includes the default JSON renderer output for our Category
resource, as well as a _self
link so clients can request the category directly if needed.
Let’s use the embedded method on the `order/show.gson
view, and embed the order.customer
resource:
import com.example.Order
model {
Order order
}
json {
hal.links(self: order, customer: order.customer)
hal.embedded(customer: order.customer) (1)
Make a request to http://localhost:8080/api/orders/1
:
}
},
"_embedded": {
"customer": {
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/customers/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"firstName": "Peter",
"lastName": "River",
"version": 0
Now clients of our API can access the details of embedded resources, without making additional requests.
//examples are using the fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
//retrieve an order instance from the API
fetch("http://localhost:8080/api/orders/1").then(function(response) {
return response.json();
}).then(function(data) {
this.order = data;
});
var customer = this.order._embedded.customer;
console.log("Order ID: " + this.order.id);
console.log("Customer: " + customer.firstName + " " + customer.lastName);
//retrieve a product instance from the API
fetch("http://localhost:8080/api/products/1").then(function(response) {
return response.json();
}).then(function(data) {
this.product = data;
});
console.log("Product: " + this.product.name);
console.log("Category:" + this.order._embedded.category.name);
5.4 Paginate Results
Another convention specified by the HAL standard is pagination. When serving a list of resources, the _links
element can provide first
, prev
, next
and last
links, which can be used to navigate the resource list.
The hal
helper provides a paginate
method that will generate these links and handle the pagination of resources. This method requires a bit more information in the model
of our JSON view, in order to keep track of the current offset, max number of records per page, and the total number of resources. In order to do this, we’ll need to create a controller so that we can pass in the needed model parameters.
Let’s use HAL pagination links on our Product
resource.
Because we’ll be creating our own ProductController
to supply the parameters needed for pagination, we’ll need to remove the @Resource
annotation we’ve been using on our Product
domain class. Edit grails-app/domain/com/example/Product.groovy
:
package com.example
class Product {
String name
String inventoryId
BigDecimal price
static belongsTo = [ category : Category ]
}
You’ll often find when developing a RESTful API that the generated controllers and URL mappings from @Resource are a great way to get started, but at some point you’ll want more control over the API - generating the RestfulController yourself is a good solution at that point.
|
Now, create a new RestfulController
using the create-restful-controller
command (supplied by the rest-api
profile):
$ ./grailsw create-restful-controller com.example.ProductController
Edit this new controller with the following content:
package com.example
import grails.rest.RestfulController
class ProductController extends RestfulController<Product> {
static responseFormats = ['json']
ProductController() {
super(Product)
}
@Override
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
return [
productList : listAllResources(params), (1)
productCount: countResources(), (2)
max : params.max, (3)
offset : params.int("offset") ?: 0, (4)
sort : params.sort, (5)
order : params.order (6)
]
}
@Override
boolean getReadOnly() {
return true
}
}
1 | listAllResource() is a method provided by RestfulController to return a list of all domain resources - you can override this method to control how this list is generated |
2 | countResources() is another RestfulController method - again, you can override the implementation to suite your API |
3 | Total number of results per page |
4 | Offset (used to calculate current page) |
5 | Property to sort by |
6 | Direction of sorting |
Finally, we need to edit our UrlMappings
to create the rest endpoints that were formerly generated using the @Resource
annotation. Grails supports a resource
property on URL mappings that will generate these URLs automatically. Edit UrlMappings.groovy
and add the following rule to the mappings
block:
"/api/products"(resources: "product")
Now, we can create our new JSON view using pagination. Create the following view and template under grails-app/views/product
:
import com.example.Product
model {
Iterable<Product> productList (1)
Integer productCount (2)
Integer max
Integer offset
String sort
String order
}
json {
hal.paginate(Product, productCount, offset, max, sort, order) (3)
products tmpl.product(productList ?: [])
count productCount
max max
offset offset
sort sort
order order
}
1 | List of product resources |
2 | Pagination params from our controller |
3 | Here we pass the pagination parameters to the paginate method, which will generate the HAL pagination links |
import com.example.Product
model {
Product product
}
json {
hal.links(product)
name product.name
id product.inventoryId
price product.price
category product.category.name
}
Make a request to http://locahost:8080/api/products
:
"""
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products?offset=0&max=10",
"hreflang": "en",
"type": "application/hal+json"
},
"first": {
"href": "http://localhost:${serverPort}/api/products?offset=0&max=10",
"hreflang": "en"
},
"next": {
"href": "http://localhost:${serverPort}/api/products?offset=10&max=10",
"hreflang": "en"
},
"last": {
"href": "http://localhost:${serverPort}/api/products?offset=10&max=10",
"hreflang": "en"
}
},
"products": [
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/1",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Cargo Pants",
"id": "CLOTH001",
"price": 15.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/2",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Sweater",
"id": "CLOTH002",
"price": 12.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/3",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Jeans",
"id": "CLOTH003",
"price": 15.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/4",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Blouse",
"id": "CLOTH004",
"price": 18.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/5",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "T-Shirt",
"id": "CLOTH005",
"price": 10.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/6",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Jacket",
"id": "CLOTH006",
"price": 20.00,
"category": "Clothing"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/7",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Bookcase",
"id": "FURN001",
"price": 40.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/8",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Coffee Table",
"id": "FURN002",
"price": 50.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/9",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Vanity",
"id": "FURN003",
"price": 90.00,
"category": "Furniture"
},
{
"_links": {
"self": {
"href": "http://localhost:${serverPort}/api/products/10",
"hreflang": "en",
"type": "application/hal+json"
}
},
"name": "Table Saw",
"id": "TOOL001",
"price": 120.00,
"category": "Tools"
}
],
"count": 13,
"max": 10,
"offset": 0,
"sort": null,
"order": null
}
Make a request to the next
link, http://localhost:8080/product/index?offset=10&max=10
, and you’ll see the next page of results. Due to the small number of resources in our sample data there will only be 2 pages - try changing the max
parameter in your request to 4 - you’ll now retrieve additional pages to reflect the smaller page size.
If you’d like, repeat these steps to enable pagination for the other domain resources, such as Order
and Customer
.
5.5 Custom MIME Types
HAL resources can declare a custom "MIME Type" (or "Content Type") that clients should use in order to interact with the API. Grails includes two generic HAL MIME types in the default application.yml
:
accept:
header:
userAgents:
- Gecko
- WebKit
- Presto
- Trident
types:
json:
- application/json
- text/json
hal:
- application/hal+json
- application/hal+xml
xml:
- text/xml
- application/xml
You can specify a custom MIME type for your API if you wish, by adding an entry to this configuration:
xml:
- text/xml
- application/xml
atom: application/atom+xml
css: text/css
csv: text/csv
js: text/javascript
rss: application/rss+xml
text: text/plain
all: '*/*'
inventory: "application/vnd.com.example.inventory+json" (1)
urlmapping:
cache:
maxsize: 1000
1 | Specifies a MIME type called inventory , and gives it a type specification (by convention, vnd indicates a "vendor" MIME type) |
Now you can use this custom MIME type in your JSON views, using the type
helper method:
import com.example.Product
model {
Product product
}
json {
hal.links(product)
hal.embedded(category: product.category)
hal.type("inventory") (1)
id product.inventoryId
name product.name
price product.price
}
1 | The hal.type() method takes a string to identify the custom MIME type in the application.yml file, or an explicit MIME specification as a string |
Make a request to http://localhost:8080/api/products/1
to see the custom content-type:
$ curl -i localhost:8080/product/1
HTTP/1.1 200
X-Application-Context: application:development
Content-Type: application/vnd.com.example.inventory+json;charset=UTF-8 (1)
Transfer-Encoding: chunked
Date: Sun, 05 Feb 2017 01:51:08 GMT
def result = render(view: "/product/show", model:[product: product])
then:
result.json._embedded
}
}
1 | Note the new MIME type in the Content-Type header |
6 Explore the API
HAL+JSON is a flexible and powerful specification for structuring your API’s output. It allows clients of your API - whether third-party integrations or your own webapps or native apps - to navigate your resources in an efficient and consistent way.
There are many useful tools for developing and testing HAL+JSON APIs. If you use the Google Chrome browser, try installing the JSONView plugin, and navigating your browser to http://localhost:8080/api/products
. This plugin will render the JSON in a pleasant format, and allow you to follow links between your resources in your browser.
Another powerful tool for testing and your API is Postman, a Google Chrome-based app available for macOS, Windows, Linux and Chrome OS.