5 Make our API Discoverable via HAL

We’ve customized our API somewhat now, but we have a few issues:

  1. 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)

  2. 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)

  3. 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.

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:

grails-app/views/order/show.gson
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.
{
    _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:

grails-app/views/order/_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:

grails-app/views/order/show_v3.gson
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:

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:

grails-app/views/customer/show.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:

  1. We could simply include the category details in our JSON view: This approach obscures the boundary between the Product and Category resources in our API - it gives the (incorrect) impression to the client that category.name is a property of Product, rather than a API resource in its own right.

  2. 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:

grails-app/views/product/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:

grails-app/views/order/show.gson
import com.example.Order

model {
    Order order
}
json {
    hal.links(self: order, customer: order.customer)
    hal.embedded(customer: order.customer) (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:

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:

grails-app/controllers/com/example/ProductController.groovy
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:

grails-app/controller/com/example/UrlMappings.groovy
"/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:

grails-app/views/product/index.gson
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
grails-app/views/product/_product.gson
import com.example.Product

model {
    Product product
}
json {
    hal.links(product)

    name product.name
    id product.inventoryId
    price product.price
    category product.category.name
}
"""
{
  "_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:

grails-app/conf/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:

grails-app/conf/application.yml
        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:

grails-app/views/product/show.gson
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
  Get the Code