Spring HATEOAS Tutorial

Learn to build hateoas links for REST resources using RepresentationModel and RepresentationModelAssemblerSupport in a Spring boot application.

1. Spring HATEOAS 1.x Changes

Spring HATEAOS 1.x module has gone through some major changes including package structure and class names, as compared to older version. Let’s discuss some important classes now used in 1.x.

1.1. Important classes

  • RepresentationModel – is a container for a collection of Links and provides APIs to add those links to the model.
  • EntityModel – represents RepresentationModel containing only single entity and related links.
    public class ActorModel 
    	extends RepresentationModel<ActorModel> 
    {
    	// attributes
    }
    
  • CollectionModel – is a wrapper for a collection of entities (entity as well as collection). To create collection models, use it’ constructors (e.g. CollectionModel(List) or CollectionModel(Iterable)) or toCollectionModel() provided by model assemblers.
    public class AlbumModelAssembler 
    	extends RepresentationModelAssemblerSupport<AlbumEntity, AlbumModel> 
    {
    	@Override
    	public CollectionModel<AlbumModel> toCollectionModel(Iterable<? extends AlbumEntity> entities) 
    	{
    		CollectionModel<AlbumModel> actorModels = super.toCollectionModel(entities);
    	}
    }
    
  • PagedModel – is similar to CollectionModel with underlying pageable collection of entities.
  • RepresentationModelAssembler – It’s implementation classes (such as RepresentationModelAssemblerSupport) provides methods to convert a domain object into a RepresentationModel.
  • WebMvcLinkBuilder – It helps to ease building Link instances pointing to Spring MVC controllers.
    Link lnk = WebMvcLinkBuilder
    			.linkTo(WebMvcLinkBuilder.methodOn(WebController.class)
    			.getAllAlbums())
    			.withSelfRel();
    
  • Link – represents a single link added to representation model.
  • LinkRelationProvider – provides API to add link relations ("rel" type) in Link instances.

The recommeded way to work with representation model is :

  • extend the domain class with RepresentationModel
  • create instances of this class
  • populate the properties and enrich it with links

1.2. Creating links

In Spring web MVC and webflux applications, we can use WebMvcLinkBuilder to create links pointing to controller classes and it’s methods.

//Controller class

@Controller
class EmployeeController {

  @GetMapping("/employees")
  HttpEntity<CollectionModel<EmployeeModel>> getAllEmployees() { … }

  @GetMapping(value = "/employees/{id}")
  HttpEntity<EmployeeModel> getEmployeeById(@PathVariable Long id) { … }
}

//Create link

import static org.sfw.hateoas.server.mvc.WebMvcLinkBuilder.*;

//Method 1

Link link = linkTo(methodOn(EmployeeController.class)
				.getAllEmployees())
				.withRel("employees");

// Method 2

Method method = WebController.class.getMethod("getActorById", Long.class);
Link link = linkTo(method, 2L).withSelfRel();

1.3. HAL – Hypertext application language

JSON Hypertext Application Language or HAL is one of the simplest and most widely adopted hypermedia media types

By default, Spring hateoas generated responses are in application/hal+json format. It is the default mediatype even if we pass application/json as well.

In HAL, the _links entry is a JSON object. The property names are link relations and each value is single or multiple links.

"_links": {
	"self": {
		"href": "http://localhost:8080/api/actors/1"
	}
}

2. Spring HATEOAS RepresentationModel Example

To demonstrate the usage of representation models, we will create a Spring web MVC application providing albums and their actors’ basic information. An album can have many actors. Similarily, an actor can be associated with multiple albums.

We will fetch create 4 REST apis to provide:

  • /api/actors – List of all actors
  • /api/actors/{id} – An actor given by id.
  • /api/albums – List of all albums.
  • /api/albums/{id} – An album given by id.

Responses of all APIs will have links added using spring hateoas classes.

2.1. Dependencies

<?xml version="1.0"?>
<project
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
	xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
	<modelVersion>4.0.0</modelVersion>
	
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath />
	</parent>
	
	<groupId>com.springexamples</groupId>
	<artifactId>boot-hateoas</artifactId>
	<name>boot-hateoas</name>
	<url>http://maven.apache.org</url>
	
	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<java.version>1.8</java.version>
	</properties>
	
	<dependencies>
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</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-hateoas</artifactId>
		</dependency>

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

	</dependencies>

</project>

2.2. JPA entities and repositories

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString(exclude = "albums")
@Entity
@Table(name="actor")
public class ActorEntity implements Serializable 
{
	private static final long serialVersionUID = 1L;
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String firstName;
	private String lastName;
	private String birthDate;
	
	@ManyToMany(cascade=CascadeType.ALL)
	@JoinTable(
	  name = "actor_album", 
	  joinColumns = @JoinColumn(name = "actor_id"), 
	  inverseJoinColumns = @JoinColumn(name = "album_id"))
	private List<AlbumEntity> albums;
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
@ToString(exclude = "actors")
@Table(name="album")
public class AlbumEntity implements Serializable 
{
	private static final long serialVersionUID = 1L;
	
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	private String title;
	private String description;
	private String releaseDate;
	
	@ManyToMany(mappedBy = "albums",fetch = FetchType.EAGER)
	private List<ActorEntity> actors;
}
public interface ActorRepository 
	extends JpaRepository<ActorEntity, Long>{
}
public interface AlbumRepository 
	extends JpaRepository<AlbumEntity, Long>{

}

The corresponding data is loaded in H2 inmemory database using given files and configuration.

spring.datasource.url=jdbc:h2:mem:test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

spring.jpa.hibernate.ddl-auto=none
CREATE TABLE actor (
	id INT PRIMARY KEY,
	first_name VARCHAR(255) NULL,
	last_name VARCHAR(255) NULL,
	birth_date VARCHAR(255) NULL
);

CREATE TABLE album (
	id INT PRIMARY KEY,
	title VARCHAR(255) NULL,
	description VARCHAR(255) NULL,
	release_date VARCHAR(255) NULL
);

CREATE TABLE actor_album (
	actor_id INT,
	album_id INT
);
INSERT INTO actor VALUES ('1', 'John', 'Doe', '10-Jan-1952');
INSERT INTO actor VALUES ('2', 'Amy', 'Eugene', '05-07-1985');
INSERT INTO actor VALUES ('3', 'Laverne', 'Mann', '11-12-1988');
INSERT INTO actor VALUES ('4', 'Janice', 'Preston', '19-02-1960');
INSERT INTO actor VALUES ('5', 'Pauline', 'Rios', '29-08-1977');

INSERT INTO album VALUES ('1', 'Top Hits Vol 1', 'Top hits vol 1. description', '10-03-1981');
INSERT INTO album VALUES ('2', 'Top Hits Vol 2', 'Top hits vol 2. description', '10-03-1982');
INSERT INTO album VALUES ('3', 'Top Hits Vol 3', 'Top hits vol 3. description', '10-03-1983');
INSERT INTO album VALUES ('4', 'Top Hits Vol 4', 'Top hits vol 4. description', '10-03-1984');
INSERT INTO album VALUES ('5', 'Top Hits Vol 5', 'Top hits vol 5. description', '10-03-1985');
INSERT INTO album VALUES ('6', 'Top Hits Vol 6', 'Top hits vol 6. description', '10-03-1986');
INSERT INTO album VALUES ('7', 'Top Hits Vol 7', 'Top hits vol 7. description', '10-03-1987');
INSERT INTO album VALUES ('8', 'Top Hits Vol 8', 'Top hits vol 8. description', '10-03-1988');
INSERT INTO album VALUES ('9', 'Top Hits Vol 9', 'Top hits vol 9. description', '10-03-1989');
INSERT INTO album VALUES ('10', 'Top Hits Vol 10', 'Top hits vol 10. description', '10-03-1990');

INSERT INTO actor_album VALUES (1, 1);
INSERT INTO actor_album VALUES (1, 2);
INSERT INTO actor_album VALUES (2, 3);
INSERT INTO actor_album VALUES (2, 4);
INSERT INTO actor_album VALUES (3, 5);
INSERT INTO actor_album VALUES (3, 6);
INSERT INTO actor_album VALUES (4, 7);
INSERT INTO actor_album VALUES (4, 8);
INSERT INTO actor_album VALUES (5, 9);
INSERT INTO actor_album VALUES (5, 10);

2.3. Model classes

These are DTO objects which will be returned from controller classes as representation models.

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonRootName(value = "actor")
@Relation(collectionRelation = "actors")
@JsonInclude(Include.NON_NULL)
public class ActorModel extends RepresentationModel<ActorModel> 
{
	private Long id;
	private String firstName;
	private String lastName;
	private String birthDate;
	
	private List<AlbumModel> albums;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@JsonRootName(value = "album")
@Relation(collectionRelation = "albums")
@JsonInclude(Include.NON_NULL)
public class AlbumModel extends RepresentationModel<AlbumModel>
{
	private Long id;
	private String title;
	private String description;
	private String releaseDate;
	
	private List<ActorModel> actors;
}

2.4. Representation model assemblers

These assemblers will be used to convert the JPA entity classes to DTO objects (entity and collection representations). i.e.

  • ActorEntity to ActorModel
  • AlbumEntity to AlbumModel

Here, we are using RepresentationModelAssemblerSupport class which implements RepresentationModelAssembler interface. It provides toModel() and toCollectionModel() methods.

@Component
public class AlbumModelAssembler 
	extends RepresentationModelAssemblerSupport<AlbumEntity, AlbumModel> {

	public AlbumModelAssembler() {
		super(WebController.class, AlbumModel.class);
	}

	@Override
	public AlbumModel toModel(AlbumEntity entity) 
	{
		AlbumModel albumModel = instantiateModel(entity);
		
		albumModel.add(linkTo(
				methodOn(WebController.class)
				.getActorById(entity.getId()))
				.withSelfRel());
		
		albumModel.setId(entity.getId());
		albumModel.setTitle(entity.getTitle());
		albumModel.setDescription(entity.getDescription());
		albumModel.setReleaseDate(entity.getReleaseDate());
		albumModel.setActors(toActorModel(entity.getActors()));
		return albumModel;
	}
	
	@Override
	public CollectionModel<AlbumModel> toCollectionModel(Iterable<? extends AlbumEntity> entities) 
	{
		CollectionModel<AlbumModel> actorModels = super.toCollectionModel(entities);
		
		actorModels.add(linkTo(methodOn(WebController.class).getAllAlbums()).withSelfRel());
		
		return actorModels;
	}

	private List<ActorModel> toActorModel(List<ActorEntity> actors) {
		if (actors.isEmpty())
			return Collections.emptyList();

		return actors.stream()
				.map(actor -> ActorModel.builder()
						.id(actor.getId())
						.firstName(actor.getFirstName())
						.lastName(actor.getLastName())
						.build()
						.add(linkTo(
								methodOn(WebController.class)
								.getActorById(actor.getId()))
								.withSelfRel()))
				.collect(Collectors.toList());
	}
}
@Component
public class ActorModelAssembler 
	extends RepresentationModelAssemblerSupport<ActorEntity, ActorModel> { 

	public ActorModelAssembler() {
		super(WebController.class, ActorModel.class);
	}

	@Override
	public ActorModel toModel(ActorEntity entity) 
	{
		ActorModel actorModel = instantiateModel(entity);
		
		actorModel.add(linkTo(
				methodOn(WebController.class)
				.getActorById(entity.getId()))
				.withSelfRel());
		
		actorModel.setId(entity.getId());
		actorModel.setFirstName(entity.getFirstName());
		actorModel.setLastName(entity.getLastName());
		actorModel.setBirthDate(entity.getBirthDate());
		actorModel.setAlbums(toAlbumModel(entity.getAlbums()));
		return actorModel;
	}
	
	@Override
	public CollectionModel<ActorModel> toCollectionModel(Iterable<? extends ActorEntity> entities) 
	{
		CollectionModel<ActorModel> actorModels = super.toCollectionModel(entities);
		
		actorModels.add(linkTo(methodOn(WebController.class).getAllActors()).withSelfRel());
		
		return actorModels;
	}

	private List<AlbumModel> toAlbumModel(List<AlbumEntity> albums) {
		if (albums.isEmpty())
			return Collections.emptyList();

		return albums.stream()
				.map(album -> AlbumModel.builder()
						.id(album.getId())
						.title(album.getTitle())
						.build()
						.add(linkTo(
								methodOn(WebController.class)
								.getAlbumById(album.getId()))
								.withSelfRel()))
				.collect(Collectors.toList());
	}
}

2.5. REST Controller

The REST controller having APIs is :

@RestController
public class WebController {

	@Autowired
	private AlbumRepository albumRepository;
	
	@Autowired
	private ActorRepository actorRepository;
	
	@Autowired
	private ActorModelAssembler actorModelAssembler;
	
	@Autowired
	private AlbumModelAssembler albumModelAssembler;
	
	@GetMapping("/api/actors")
	public ResponseEntity<CollectionModel<ActorModel>> getAllActors() 
	{
		List<ActorEntity> actorEntities = actorRepository.findAll();
		return new ResponseEntity<>(
				actorModelAssembler.toCollectionModel(actorEntities), 
				HttpStatus.OK);
	}
	
	@GetMapping("/api/actors/{id}")
	public ResponseEntity<ActorModel> getActorById(@PathVariable("id") Long id) 
	{
		return actorRepository.findById(id) 
				.map(actorModelAssembler::toModel) 
				.map(ResponseEntity::ok) 
				.orElse(ResponseEntity.notFound().build());
	}
	
	@GetMapping("/api/albums")
	public ResponseEntity<CollectionModel<AlbumModel>> getAllAlbums() 
	{
		List<AlbumEntity> albumEntities = albumRepository.findAll();
		
		return new ResponseEntity<>(
				albumModelAssembler.toCollectionModel(albumEntities), 
				HttpStatus.OK);
	}
		
	@GetMapping("/api/albums/{id}")
	public ResponseEntity<AlbumModel> getAlbumById(@PathVariable("id") Long id) 
	{
		return albumRepository.findById(id) 
				.map(albumModelAssembler::toModel) 
				.map(ResponseEntity::ok) 
				.orElse(ResponseEntity.notFound().build());
	}
}

2.6. Run the application

Run the application as Spring boot application and observe the outputs.

@SpringBootApplication
public class SpringBootHateoasApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringBootHateoasApplication.class, args);
	}
}
{
  "_embedded": {
    "albums": [
      {
        "id": 1,
        "title": "Top Hits Vol 1",
        "description": "Top hits vol 1. description",
        "releaseDate": "10-03-1981",
        "actors": [
          {
            "id": 1,
            "firstName": "John",
            "lastName": "Doe",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/1"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/1"
          }
        }
      },
      {
        "id": 2,
        "title": "Top Hits Vol 2",
        "description": "Top hits vol 2. description",
        "releaseDate": "10-03-1982",
        "actors": [
          {
            "id": 1,
            "firstName": "John",
            "lastName": "Doe",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/1"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/2"
          }
        }
      },
      {
        "id": 3,
        "title": "Top Hits Vol 3",
        "description": "Top hits vol 3. description",
        "releaseDate": "10-03-1983",
        "actors": [
          {
            "id": 2,
            "firstName": "Amy",
            "lastName": "Eugene",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/2"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/3"
          }
        }
      },
      {
        "id": 4,
        "title": "Top Hits Vol 4",
        "description": "Top hits vol 4. description",
        "releaseDate": "10-03-1984",
        "actors": [
          {
            "id": 2,
            "firstName": "Amy",
            "lastName": "Eugene",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/2"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/4"
          }
        }
      },
      {
        "id": 5,
        "title": "Top Hits Vol 5",
        "description": "Top hits vol 5. description",
        "releaseDate": "10-03-1985",
        "actors": [
          {
            "id": 3,
            "firstName": "Laverne",
            "lastName": "Mann",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/3"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/5"
          }
        }
      },
      {
        "id": 6,
        "title": "Top Hits Vol 6",
        "description": "Top hits vol 6. description",
        "releaseDate": "10-03-1986",
        "actors": [
          {
            "id": 3,
            "firstName": "Laverne",
            "lastName": "Mann",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/3"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/6"
          }
        }
      },
      {
        "id": 7,
        "title": "Top Hits Vol 7",
        "description": "Top hits vol 7. description",
        "releaseDate": "10-03-1987",
        "actors": [
          {
            "id": 4,
            "firstName": "Janice",
            "lastName": "Preston",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/4"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/7"
          }
        }
      },
      {
        "id": 8,
        "title": "Top Hits Vol 8",
        "description": "Top hits vol 8. description",
        "releaseDate": "10-03-1988",
        "actors": [
          {
            "id": 4,
            "firstName": "Janice",
            "lastName": "Preston",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/4"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/8"
          }
        }
      },
      {
        "id": 9,
        "title": "Top Hits Vol 9",
        "description": "Top hits vol 9. description",
        "releaseDate": "10-03-1989",
        "actors": [
          {
            "id": 5,
            "firstName": "Pauline",
            "lastName": "Rios",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/5"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/9"
          }
        }
      },
      {
        "id": 10,
        "title": "Top Hits Vol 10",
        "description": "Top hits vol 10. description",
        "releaseDate": "10-03-1990",
        "actors": [
          {
            "id": 5,
            "firstName": "Pauline",
            "lastName": "Rios",
            "_links": {
              "self": {
                "href": "http://localhost:8080/api/actors/5"
              }
            }
          }
        ],
        "_links": {
          "self": {
            "href": "http://localhost:8080/api/actors/10"
          }
        }
      }
    ]
  },
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/albums"
    }
  }
}
{
  "id": 1,
  "title": "Top Hits Vol 1",
  "description": "Top hits vol 1. description",
  "releaseDate": "10-03-1981",
  "actors": [
    {
      "id": 1,
      "firstName": "John",
      "lastName": "Doe",
      "_links": {
        "self": {
          "href": "http://localhost:8080/api/actors/1"
        }
      }
    }
  ],
  "_links": {
    "self": {
      "href": "http://localhost:8080/api/actors/1"
    }
  }
}

Similarly, you can see the output of other APIs as well.

3. Conclusion

In this spring hateoas tutorial, we learned :

  • Important classes in spring hateoas module
  • What are representation models
  • How to create entity and collection models
  • How to insert links in model using contoller method references
  • and finally, how to use representation model assemblers

Drop me your questions in comments.

Happy Learning !!

Reference : Spring hateoas docs

Was this post helpful?

Join 7000+ Fellow Programmers

Subscribe to get new post notifications, industry updates, best practices, and much more. Directly into your inbox, for free.

11 thoughts on “Spring HATEOAS Tutorial”

  1. I would wish to appreciate you for this great contribution. I am new to using hateoas leave alone spring boot but I have to say your post has pushed me far. I would like to ask you for some example code for a microservice client. Thank you so much

    Reply
  2. hi! Hi, I’m having this error when I try to start spring boot. Do you have any idea that it could be failed?

    Parameter 0 of method linkDiscoverers in org.springframework.hateoas.config.HateoasConfiguration 
    required a single bean, but 17 were found:
    	- modelBuilderPluginRegistry: defined in null
    	- modelPropertyBuilderPluginRegistry: defined in null
    	- typeNameProviderPluginRegistry: defined in null
    	- syntheticModelProviderPluginRegistry: defined in null
    	- documentationPluginRegistry: defined in null
    	- apiListingBuilderPluginRegistry: defined in null
    	- operationBuilderPluginRegistry: defined in null
    	- parameterBuilderPluginRegistry: defined in null
    	- expandedParameterBuilderPluginRegistry: defined in null
    	- resourceGroupingStrategyRegistry: defined in null
    	- operationModelsProviderPluginRegistry: defined in null
    	- defaultsProviderPluginRegistry: defined in null
    	- pathDecoratorRegistry: defined in null
    	- apiListingScannerPluginRegistry: defined in null
    	- relProviderPluginRegistry: defined by method 'relProviderPluginRegistry' 
    in class path resource [org/springframework/hateoas/config/HateoasConfiguration.class]
    	- linkDiscovererRegistry: defined in null
    	- entityLinksPluginRegistry: defined by method 'entityLinksPluginRegistry' 
    in class path resource [org/springframework/hateoas/config/WebMvcEntityLinksConfiguration.class]
    Reply
  3. I don’t see you’ve configured the pagination configurations, wondering how custom pagination will work here ?
    Ideally, since this is pagination implementation, one should used PageResourceAssembler, without pagination things will not work here.

    Reply

Leave a Comment

HowToDoInJava

A blog about Java and its related technologies, the best practices, algorithms, interview questions, scripting languages, and Python.