This guide will help you implement HATEOAS for a REST API/Service with Spring Boot.
You Will Learn
- What is HATEOAS?
- Why do you need HATEOAS?
- How do you implement HATEOAS with Spring Boot?
- What are the HATEOAS best practices?
10 Step Reference Courses
- Spring Framework for Beginners in 10 Steps
- Spring Boot for Beginners in 10 Steps
- Spring MVC in 10 Steps
- JPA and Hibernate in 10 Steps
- Eclipse Tutorial for Beginners in 5 Steps
- Maven Tutorial for Beginners in 5 Steps
- JUnit Tutorial for Beginners in 5 Steps
- Mockito Tutorial for Beginners in 5 Steps
- Complete in28Minutes Course Guide
Project Code Structure
Following screenshot shows the structure of the project we will create.
A few details:
- SpringBoot2RestServiceApplication.java - The Spring Boot Application class generated with Spring Initializer. This class acts as the launching point for the application.
pom.xml
- Contains all the dependencies needed to build this project. We will use Spring Boot Starter AOP.Student.java
- Student JPA Entity- StudentRepository.java - Student JPA Repository. This is created using Spring Data JpaRepository.
- StudentResource.java - Spring Rest Controller exposing all services on the student resource.
- data.sql - Initial data for the student table. Spring Boot would execute this script after the tables are created from the entities.
- Maven 3.0+ is your build tool
- Your favorite IDE. We use Eclipse.
- JDK 1.8+
Complete Maven Project With Code Examples
Our Github repository has all the code examples - https://github.com/in28minutes/spring-boot-examples/tree/master/spring-boot-2-rest-service-hateoas
Richardson Maturity Model
Richardson Maturity Model defines the maturity level of a Restful Web Service. Following are the different levels and their characteristics.
- Level 0: Expose SOAP web services in REST style. Expose action based services (http://server/getPosts, http://server/deletePosts, http://server/doThis, http://server/doThat etc) using REST.
- Level 1: Expose Resources with proper URI’s (using nouns). Ex: http://server/accounts, http://server/accounts/10. However, HTTP Methods are not used.
- Level 2: Resources use proper URI’s + HTTP Methods. For example, to update an account, you do a PUT. To create an account, you do a POST. Uri’s look like posts/1/comments/5 and accounts/1/friends/1.
- Level 3: HATEOAS (Hypermedia as the engine of application state). You will tell not only about the information being requested but also about the next possible actions that the service consumer can do. When requesting information about a Facebook user, a REST service can return user details along with information about how to get his recent posts, how to get his recent comments and how to retrieve his friend’s list.
What Is HATEOAS?
HATEOAS stands for “Hypermedia as the engine of application state.” It's a complicated acronym. Let’s decode it for you.
What do you see when you visit a web page?
The data that you would want to see. Is that all? You would also see links and buttons to see related data.
For example, if you go to a student page, you will see
- Student profile
- Links to Edit and Delete Student details
- Links to see details of other students
- Link to see details of the courses and grades of the student
HATEOAS brings the same concepts to RESTful Web Services.
When some details of a resource are requested, you will provide the resource details as well as details of related resources and the possible actions you can perform on the resource. For example, when requesting information about a Facebook user, a REST service can return the following
- User details
- Links to get his recent posts
- Links to get his recent comments
- Links to retrieve his friend’s list.
Bootstrapping a Project With a REST Resource
In the previous article in the series, we set up a simple RESTful service with a resource exposing CRUD methods.
We will use the same example to discuss HATEOAS.
Implementing HATEOAS With Spring Boot
Spring Boot provides a Starter for HATEOAS. Include the dependency in your pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
Components in Spring Boot HATEOAS Starter
Listed below are some of the important dependencies from spring-boot-starter-hateoas
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.0.0.M6</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>0.24.0.RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>1.2.0.RELEASE</version>
<scope>compile</scope>
</dependency>
The most important dependency is spring-hateoas
.
Enhancing the Resource to Return HATEOAS Response
To implement HATEOAS, we would need to include related resources in the response.
Instead of Student, we use a return type of Resource<Student>
. Resource is a simple class wrapping a domain object and allows adding links to it.
@GetMapping("/students/{id}")
public Resource<Student> retrieveStudent(@PathVariable long id) {
Optional<Student> student = studentRepository.findById(id);
if (!student.isPresent())
throw new StudentNotFoundException("id-" + id);
Resource<Student> resource = new Resource<Student>(student.get());
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());
resource.add(linkTo.withRel("all-students"));
return resource;
}
We create a new resource.
Resource<Student> resource = new Resource<Student>(student.get());
We add the link to retrieve all students method to the links.
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());
resource.add(linkTo.withRel("all-students"));
The response when we execute a GET request to http://localhost:8080/students/10001
is shown below:
{
"id": 10001,
"name": "Ranga",
"passportNumber": "E1234567",
"_links": {
"all-students": {
"href": "http://localhost:8080/students"
}
}
}
You can see that there is a new section _links
with a link to all students Resource.
Enhance Other Resources With HATEOAS
Above example covers important concepts in enhancing resources with HATEOAS.
However, you have to make the important decision:
- What are the important resources related to a specific resource?
Go ahead and enhance the application with more HATEOAS links.
Next Steps
- Learn Basics of Spring Boot - Spring Boot vs Spring vs Spring MVC, Auto Configuration, Spring Boot Starter Projects, Spring Boot Starter Parent, Spring Boot Initializr
- Learn RESTful and SOAP Web Services with Spring Boot
- Learn Microservices with Spring Boot and Spring Cloud
- Watch Spring Framework Interview Guide - 200+ Questions & Answers
Complete Code Example
/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.in28minutes.springboot.rest.example</groupId>
<artifactId>spring-boot-2-rest-service-hateoas</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-2-rest-service</name>
<description>Spring Boot 2 and REST - Example Project</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.M6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
/src/main/java/com/in28minutes/springboot/rest/example/SpringBoot2RestServiceApplication.java
package com.in28minutes.springboot.rest.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBoot2RestServiceApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBoot2RestServiceApplication.class, args);
}
}
/src/main/java/com/in28minutes/springboot/rest/example/student/Student.java
package com.in28minutes.springboot.rest.example.student;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
public class Student {
@Id
@GeneratedValue
private Long id;
private String name;
private String passportNumber;
public Student() {
super();
}
public Student(Long id, String name, String passportNumber) {
super();
this.id = id;
this.name = name;
this.passportNumber = passportNumber;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassportNumber() {
return passportNumber;
}
public void setPassportNumber(String passportNumber) {
this.passportNumber = passportNumber;
}
}
/src/main/java/com/in28minutes/springboot/rest/example/student/StudentNotFoundException.java
package com.in28minutes.springboot.rest.example.student;
public class StudentNotFoundException extends RuntimeException {
public StudentNotFoundException(String exception) {
super(exception);
}
}
/src/main/java/com/in28minutes/springboot/rest/example/student/StudentRepository.java
package com.in28minutes.springboot.rest.example.student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long>{
}
/src/main/java/com/in28minutes/springboot/rest/example/student/StudentResource.java
package com.in28minutes.springboot.rest.example.student;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import java.net.URI;
import java.util.List;
import java.util.Optional;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
@RestController
public class StudentResource {
@Autowired
private StudentRepository studentRepository;
@GetMapping("/students")
public List<Student> retrieveAllStudents() {
return studentRepository.findAll();
}
@GetMapping("/students/{id}")
public Resource<Student> retrieveStudent(@PathVariable long id) {
Optional<Student> student = studentRepository.findById(id);
if (!student.isPresent())
throw new StudentNotFoundException("id-" + id);
Resource<Student> resource = new Resource<Student>(student.get());
ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).retrieveAllStudents());
resource.add(linkTo.withRel("all-students"));
return resource;
}
@DeleteMapping("/students/{id}")
public void deleteStudent(@PathVariable long id) {
studentRepository.deleteById(id);
}
@PostMapping("/students")
public ResponseEntity<Object> createStudent(@RequestBody Student student) {
Student savedStudent = studentRepository.save(student);
URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedStudent.getId()).toUri();
return ResponseEntity.created(location).build();
}
@PutMapping("/students/{id}")
public ResponseEntity<Object> updateStudent(@RequestBody Student student, @PathVariable long id) {
Optional<Student> studentOptional = studentRepository.findById(id);
if (!studentOptional.isPresent())
return ResponseEntity.notFound().build();
student.setId(id);
studentRepository.save(student);
return ResponseEntity.noContent().build();
}
}
/src/main/resources/data.sql
insert into student
values(10001,'Ranga', 'E1234567');
insert into student
values(10002,'Ravi', 'A1234568');
/src/test/java/com/in28minutes/springboot/rest/example/SpringBoot2RestServiceApplicationTests.java
package com.in28minutes.springboot.rest.example;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBoot2RestServiceApplicationTests {
@Test
public void contextLoads() {
}
}