Programming for fun and profit

Programming tutorials, problems, solutions. Always with code.



How to create simple Spring Boot REST service with HATEOAS support?

It’s very simple with spring-boot-hateoas project.


Project configuration for Gradle (build.gradle):

apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'idea'
apply plugin: 'spring-boot'

sourceCompatibility = 1.8
targetCompatibility = 1.8

buildscript {
   repositories {
   dependencies {

jar {
   baseName = 'spring-boot-rest-hateoas-sample'
   version = '0.1.0'

repositories {

dependencies {
   // Dependencies for Spring Boot with REST HATEOAS:
   compile "com.fasterxml.jackson.core:jackson-databind"
   compile "com.jayway.jsonpath:json-path:0.9.1"
   compile "org.springframework.boot:spring-boot-starter-hateoas"

   // Dependencies for Spock tests:
   compile "org.codehaus.groovy:groovy-all:2.4.1"
   testCompile "org.spockframework:spock-core:1.0-groovy-2.4"
   testCompile "org.spockframework:spock-spring:1.0-groovy-2.4"

   testCompile "org.springframework.boot:spring-boot-starter-test"

In TDD we start with a test, so create src/test/groovy/com/farenda/solved/EchoServiceTest.groovy with the following content:

package com.farenda.solved

import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.TestRestTemplate
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.hateoas.MediaTypes
import org.springframework.hateoas.ResourceSupport
import org.springframework.hateoas.hal.Jackson2HalModule
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter
import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.test.context.ContextConfiguration
import spock.lang.Shared
import spock.lang.Specification

// Need to specify "loader" here to workaround Spock issue
// with meta annotations:
@ContextConfiguration(classes = [JavaSolved],
                      loader = SpringApplicationContextLoader)
// Tell Spring to start Web Server on random port, because
// usual 8080 may be busy:
@WebIntegrationTest(randomPort = true)
class EchoServiceTest extends Specification {

   // Inject random port of test Web Server:
   def int serverPort

   def rest = getRestTemplateWithHalMessageConverter()

   // Registered converters are processed in order, so it's
   // not enough to add the new one - we need to add it at
   // the beginning, so it can process before generic JSON
   // converter, else HAL links won't be interpreted!
   def TestRestTemplate getRestTemplateWithHalMessageConverter() {
      def restTemplate = new TestRestTemplate()
      restTemplate.getMessageConverters().add(0, halMessageConverter())
      return restTemplate

   // Needed to interpret HAL "_links". Without this
   // response will be with empty links!
   private static HttpMessageConverter halMessageConverter() {
      def objectMapper = new ObjectMapper().registerModule(new Jackson2HalModule())
      def halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport)
      return halConverter

   def 'should receive sent message with self link'() {
      // toString() is only to convert from GString to a String:
      def url = "https://localhost:${serverPort}/echo?message=".toString()
      def query = url + '{message}'
      def myMessage = 'HelloWorld!'

      // Post GET to the service and receive response as ResponseEntity.
      // We want ResponseEntity to verify HttpStatus code and links.
      // The last parameter can also be a map: [message:myMessage]
      def response = rest.getForEntity(query, EchoResource, myMessage)

      response.statusCode == HttpStatus.OK
      response.body.message == myMessage
      response.body.getLinks().size() == 1
      response.body.getLink('self').href == url+myMessage

Now, we’ve got a failing test. So here’s the service…

Spring Boot config for the app:

package com.farenda.solved;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

public class JavaSolved {
    public static void main(String[] args) throws Exception {, args);

@SpringBootApplication gives @Component, @Configuratio, and @EnableAutoConfiguration for free.

REST HATEOAS resource:

package com.farenda.solved;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.ResourceSupport;

// Created separate class for "only" message to have
// also HATEOAS "links" from ResourceSupport - see
// EchoService for usage.
public class EchoResource extends ResourceSupport {

   private final String message;

   public EchoResource(@JsonProperty("message") String message) {
      this.message = message;

   public String getMessage() {
      return message;

And, finally, Spring REST HATEOAS service:

package com.farenda.solved;

import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

// It's like @Controller, but automatically assumes @ResponseBody.
public class EchoService {

   public HttpEntity<EchoResource> echo(
          @RequestParam("message") String message) {
      EchoResource resource = new EchoResource(message);
      // Nice DSL for creating links. Here we don't have
      // other actions, so the only link is "self":
      return new ResponseEntity<>(resource, HttpStatus.OK);


You can run Spock tests as usual:

$> gradle test

Also you can start the app and verify it:

$> gradle bootRun
$> curl https://localhost:8080/echo?message=JavaSolved!
    "message": "JavaSolved!",
    "_links": {
        "self": {
            "href": "https://localhost:8080/echo?message=JavaSolved!"
Share with the World!