Spring Boot HATEOAS

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 the underlying pageable collection of entities.
  • RepresentationModelAssembler – are implementation classes (such as RepresentationModelAssemblerSupport) that provide 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 their 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 a single link 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. Similarly, an actor can be associated with multiple albums.

We will create 4 REST APIs to provide:

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

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

2.1. Dependencies

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

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 in memory 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

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

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

{
  "_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 the spring hateoas module
  • What are representation models
  • How to create entity and collection models
  • How to insert links in the model using Controller method references
  • and finally, how to use representation model assemblers

Happy Learning !!

Sourcecode on Github

Comments

Subscribe
Notify of
guest
12 Comments
Most Voted
Newest Oldest
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.