Building HATEOAS Links with Spring Boot REST

In this Spring Boot HATEOAS example, we will learn to add HATEOAS (Hypertext as the Engine of Application State) links to REST API resource representations created in a Spring boot project.

1. Spring Boot HATEOAS Module

1.1. Dependency

In a spring boot project, we need to add the spring-boot-starter-hateoas module.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

If not using Spring boot, add the spring-hateoas and spring-plugin-core dependencies.

<dependency>
    <groupId>org.springframework.hateoas</groupId>
    <artifactId>spring-hateoas</artifactId>
    <version>1.12.11</version>
</dependency>
<dependency>
    <groupId>org.springframework.plugin</groupId>
    <artifactId>spring-plugin-core</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>

1.2. Core Classes

The HATEOAS module provides given three classes to add the links to the resource representations.

  • RepresentationModel – Base class for DTOs to collect links.
  • LinkImmutable value object for links. It stores both a hypertext reference and a link relation. It exposes other attributes as defined in RFC-8288.
  • WebMvcLinkBuilder – Builder to ease building Link instances pointing to Spring MVC controllers.

1.3. Links

In general. a link contains an href and a rel attribute. Href points to the resource location and rel defines the semantics of the related resource. The media type information can be put into type attribute.

<link href="style.css" rel="stylesheet" type="text/css" />

1.3.1. Link Constructors

As stated earlier, hateoas module works with immutable Link value type. Link.of() is an overloaded method which takes various type of arguments to create an immutable instance of Link.

Link of(String href)
Link of(String href, LinkRelation relation)
Link of(String href, String relation)
Link of(UriTemplate template, LinkRelation relation)
Link of(UriTemplate template, String relation)

1.3.2. Creating Links

Lets see an example to see how Link instances are created.

Link link = Link.of("/employee-report");
assertThat(link.getHref()).isEqualTo("/employee-report");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);

link = Link.of("/employee-report", IanaLinkRelations.RELATED);
assertThat(link.getHref()).isEqualTo("/employee-report");
assertThat(link.getRel()).isEqualTo(LinkRelation.of(IanaLinkRelations.RELATED));

Please note that the href value can be a URI templates as well and we can replace the placeholder values in runtime.

Link link = Link.of("/{department}/users/{?id}");

Map<String, Object> values = new HashMap<>();
values.put("department", "HR");
values.put("id", 123);

assertThat(link.expand(values).getHref()) 
    .isEqualTo("/HR/users?id=123");

1.4. Link Relations

IanaLinkRelations class contains all the relation attribute values as per IANA (Internet Assigned Numbers Authority).

The default relation value is IanaLinkRelations.SELF. To change the link relation, we can use following code:

Link link = Link.of("/some-resource"), IanaLinkRelations.NEXT);

2. Creating Links with WebMvcLinkBuilder

Spring HATEOAS now provides a WebMvcLinkBuilder that lets us create links by pointing to controller classes.

For example, our controller class is:

@RestController
public class EmployeeController {

    @GetMapping("/employees")
    public EmployeeListVO getAllEmployees() {
    }

    @GetMapping("/employees/{id}")
    public ResponseEntity<EmployeeVO> getEmployeeById(@PathVariable("id") int id) {
    }

    @GetMapping("/employees/{id}/report")
    public ResponseEntity<EmployeeReport> getReportByEmployeeById(@PathVariable("id") int id) {
    }
}

2.1. Link.slash()

Now see how we can create various links to the controller and its methods using slash() method.

// Link to "/employees"
Link link = linkTo(EmployeeController.class).withRel("employees");

// Link to "/employees/{id}"
EmployeeVO e = new EmployeeVO(1, "Lokesh", "Gupta", "howtodoinjava@gmail.com");
Link link = linkTo(EmployeeController.class).slash(e.getId()).withSelfRel();

2.2. Method Instances / WebMvcLinkBuilder.methodOn()

We can also use create a Method instance and pas it to WebMvcLinkBuilder.

Method method = EmployeeController.class.getMethod("getReportByEmployeeById", Integer.class);
Link link = linkTo(method, 123).withSelfRel();

//or

Link link = linkTo(methodOn(EmployeeController.class).getReportByEmployeeById(123)).withSelfRel();

3. Demo

In this example, I have created three APIs with endpoints as below:

  1. /employees
  2. /employees/{id}
  3. /employees/{id}/report

3.1. Project Structure

Spring HATEOAS Example - Project Structure
Spring HATEOAS Example – Project Structure

3.2. Resource Model

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.hateoas.RepresentationModel;

@XmlRootElement(name = "employees")
public class EmployeeListVO extends RepresentationModel<EmployeeListVO>
        implements Serializable {
    private static final long serialVersionUID = 1L;

    private List<EmployeeVO> employees = new ArrayList<EmployeeVO>();

    public List<EmployeeVO> getEmployees() {
        return employees;
    }

    public void setEmployees(List<EmployeeVO> employees) {
        this.employees = employees;
    }
}
import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.hateoas.RepresentationModel;

@XmlRootElement(name = "employee")
@XmlAccessorType(XmlAccessType.NONE)
public class EmployeeVO extends RepresentationModel<EmployeeVO>
        implements Serializable {
    private static final long serialVersionUID = 1L;

    public EmployeeVO(Integer id, String firstName, String lastName,
            String email) {
        super();
        this.employeeId = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
    }

    public EmployeeVO() {

    }

    @XmlAttribute
    private Integer employeeId;

    @XmlElement
    private String firstName;

    @XmlElement
    private String lastName;

    @XmlElement
    private String email;

    //Getters and setters are hidden for brevity

    @Override
    public String toString() {
        return "EmployeeVO [id=" + employeeId + ", firstName=" + firstName
                + ", lastName=" + lastName + ", email=" + email + "]";
    }
}
import java.io.Serializable;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.hateoas.RepresentationModel;

@XmlRootElement(name = "employee-report")
public class EmployeeReport extends RepresentationModel<EmployeeReport>
        implements Serializable {

    private static final long serialVersionUID = 1L;

    // You can add field as needed
}

3.3. REST Controller Where We Add the HATEOAS Links

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import com.howtodoinjava.hateoas.demo.dao.EmployeeDB;
import com.howtodoinjava.hateoas.demo.model.EmployeeListVO;
import com.howtodoinjava.hateoas.demo.model.EmployeeReport;
import com.howtodoinjava.hateoas.demo.model.EmployeeVO;

@RestController
public class EmployeeController {

    @GetMapping("/employees")
    public EmployeeListVO getAllEmployees() {
        EmployeeListVO employeesList = new EmployeeListVO();

        for (EmployeeVO employee : EmployeeDB.getEmployeeList()) {
            // Adding self link employee 'singular' resource
            Link link = linkTo(EmployeeController.class)
                    .slash(employee.getEmployeeId()).withSelfRel();

            // Add link to singular resource
            employee.add(link);

            // Adding method link employee 'singular' resource
            ResponseEntity<EmployeeReport> methodLinkBuilder =
                    methodOn(EmployeeController.class)
                            .getReportByEmployeeById(employee.getEmployeeId());
            Link reportLink =
                    linkTo(methodLinkBuilder).withRel("employee-report");

            // Add link to singular resource
            employee.add(reportLink);

            employeesList.getEmployees().add(employee);
        }

        // Adding self link employee collection resource
        Link selfLink =
                linkTo(methodOn(EmployeeController.class).getAllEmployees())
                        .withSelfRel();

        // Add link to collection resource
        employeesList.add(selfLink);

        return employeesList;
    }

    @GetMapping("/employees/{id}")
    public ResponseEntity<EmployeeVO> getEmployeeById(
            @PathVariable("id") int id) {
        if (id <= 3) {
            EmployeeVO employee = EmployeeDB.getEmployeeList().get(id - 1);

            // Self link
            Link selfLink = linkTo(EmployeeController.class)
                    .slash(employee.getEmployeeId()).withSelfRel();

            // Method link
            Link reportLink = linkTo(methodOn(EmployeeController.class)
                    .getReportByEmployeeById(employee.getEmployeeId()))
                            .withRel("report");

            employee.add(selfLink);
            employee.add(reportLink);
            return new ResponseEntity<EmployeeVO>(employee, HttpStatus.OK);
        }
        return new ResponseEntity<EmployeeVO>(HttpStatus.NOT_FOUND);
    }

    @GetMapping("/employees/{id}/report")
    public ResponseEntity<EmployeeReport> getReportByEmployeeById(
            @PathVariable("id") int id) {
        // Do some operation and return report
        return null;
    }
}

3.4. API Responses

  • /employees
{
    "employees": [
        {
            "employeeId": 1,
            "firstName": "Lokesh",
            "lastName": "Gupta",
            "email": "howtodoinjava@gmail.com",
            "_links": {
                "self": {
                    "href": "http://localhost:8080/1"
                },
                "employee-report": {
                    "href": "http://localhost:8080/employees/1/report"
                }
            }
        },
        {
            "employeeId": 2,
            "firstName": "Amit",
            "lastName": "Singhal",
            "email": "asinghal@yahoo.com",
            "_links": {
                "self": {
                    "href": "http://localhost:8080/2"
                },
                "employee-report": {
                    "href": "http://localhost:8080/employees/2/report"
                }
            }
        },
        {
            "employeeId": 3,
            "firstName": "Kirti",
            "lastName": "Mishra",
            "email": "kmishra@gmail.com",
            "_links": {
                "self": {
                    "href": "http://localhost:8080/3"
                },
                "employee-report": {
                    "href": "http://localhost:8080/employees/3/report"
                }
            }
        }
    ],
    "_links": {
        "self": {
            "href": "http://localhost:8080/employees"
        }
    }
}
  • /employees/{id}
{
    "employeeId": 1,
    "firstName": "Lokesh",
    "lastName": "Gupta",
    "email": "howtodoinjava@gmail.com",
    "_links": {
        "self": {
            "href": "http://localhost:8080/1"
        },
        "report": {
            "href": "http://localhost:8080/employees/1/report"
        }
    }
}

4. Conclusion

As we saw in the demo that adding HATEOAS links using spring boot hateoas module is very much easy and requires very less time and effort. In return, it increases the discoverability and usefulness of APIs by many folds.

Happy Learning !!

Download Sourcecode

Leave a Reply

0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.

Our Blogs

REST API Tutorial