How to Build Hypermedia API with Spring HATEOAS




29/10/2020

near 10 min of reading

Have you ever considered the quality of your REST API? Do you know that there are several levels of REST API? Have you ever heard the term HATEOAS? Or maybe you wonder how to implement it in Java?  In this article, we answer these questions with the main emphasis on the HATEOAS concept and the implementation of that concept with the Spring HATEOAS project.

Learn more about services provided by Grape Up

You are at Grape Up blog, where our experts share their expertise gathered in projects delivered for top enterprises. See how we work.

Enabling the automotive industry to build software-defined vehicles
Empowering insurers to create insurance telematics platforms
Providing AI & advanced analytics consulting

What is HATEOAS?

Hypermedia As The Engine Of Application State – is one of the constraints of the REST architecture. Neither REST nor HATEOAS is any requirement or specification. How you implement it depends only on you. At this point, you may ask yourself – how RESTful your API is without using HATEOAS? This question is answered by the REST maturity model presented by Leonard Richardson. This model consists of four levels, as set out below:

  • Level 0
    The API implementation uses the HTTP protocol but does not utilize its full capabilities. Additionally, unique addresses for resources are not provided.
  • Level 1
    We have a unique identifier for the resource, but each action on the resource has its own URL.
  • Level 2
    We use HTTP methods instead of verbs describing actions, e.g., DELETE method instead of URL …/delete.
  • Level 3
    The term HATEOAS has been introduced. Simply speaking, it introduces hypermedia to resources. That allows you to place links in the response informing about possible actions, thereby adding the possibility to navigate through API.

Most projects these days are written using level 2. If we would like to go for the perfect RESTful API, we should consider HATEOAS.

Above,  we have an example of a response from the server in the form of JSON+HAL. Such a resource consists of two parts: our data and links to actions that are possible to be performed on a given resource.

Spring HATEOAS 1.x.x

You may be asking yourself how to implement HATEOAS in Java? You can write your solution, but why reinvent the wheel? The right tool for this seems to be the Spring Hateoas project. It is a long-standing solution on the market because its origins date back to 2012, but in 2019 we had a version 1.0 release. Of course, this version introduced a few changes compared to 0.x. They will be discussed at the end of the article after presenting some examples of using this library so that you better understand what the differences between the two versions are. Let’s discuss the possibilities of the library based on a simple API that returns us a list of movies and related directors. Our domain looks like this:

@Entity
public class Movie {
   @Id
   @GeneratedValue
   private Long id;
   private String title;
   private int year;
   private Rating rating;
   @ManyToOne
   private Director director;
}

@Entity
public class Director {
   @Id
   @GeneratedValue
   @Getter
   private Long id;
   @Getter
   private String firstname;
   @Getter
   private String lastname;
   @Getter
   private int year;
   @OneToMany(mappedBy = "director")
   private Set<Movie> movies;
}

We can approach the implementation of HATEOAS in several ways. Three methods represented here are ranked from least to most recommended.

But first, we need to add some dependencies to our Spring Boot project:

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

Ok, now we can consider implementation options.

Entity extends RepresentationModel with links directly in Controller class

Firstly, extend our entity models with RepresentationModel.

public class Movie extends RepresentationModel<Movie>
public class Director extends RepresentationModel<Director>

Then, add links to RepresentationModel within each controller. The example below returns all directors from the system. By adding two links to each director – to himself and to the entire collection. A link is also added to the collection. The key elements of this code are two methods with static imports:

  • linkTo() – responsible for creating the link
  • methodOn() – enables to dynamically generate the path to a given resource. We don’t need to hardcode the path, but we can refer to the method in the controller.
@GetMapping("/directors")
public ResponseEntity<CollectionModel<Director>> getAllDirectors() {
    List<Director> directors = directorService.getAllDirectors();
    directors.forEach(director -> {
        director.add(linkTo(methodOn(DirectorController.class).getDirectorById(director.getId())).withSelfRel());
        director.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(director.getId())).withRel("directorMovies"));
    });
    Link allDirectorsLink = linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel());
    return ResponseEntity.ok(CollectionModel.of(directors, allDirectorsLink));
}

This is the response we get after invoking such controller:

We can get a similar result when requesting a specific resource.

@GetMapping("/directors/{id}")
public ResponseEntity<Director> getDirector(@PathVariable("id") Long id) {
    return directorService.getDirectorById(id)
            .map(director -> {
                director.add(linkTo(methodOn(DirectorController.class).getDirectorById(id)).withSelfRel());
                director.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(id)).withRel("directorMovies"));
                director.add(linkTo(methodOn((DirectorController.class)).getAllDirectors()).withRel("directors"));
                return ResponseEntity.ok(director);
            })
            .orElse(ResponseEntity.notFound().build());
}

The main advantage of this implementation is simplicity. But making our entity dependent on an external library is not a very good idea. Plus, the code repetition for adding links for a specific resource is immediately noticeable. You can, of course, bring it to some private method, but there is a better way.

Use Assemblers – SimpleRepresentationModelAssembler

And it’s not about assembly language, but about a special kind of class that converts our resource to RepresentationModel.

One of such assemblers is SimpleRepresentationModelAssembler. Its implementation goes as follows:

@Component
public class DirectorAssembler implements SimpleRepresentationModelAssembler<Director> {

   @Override
   public void addLinks(EntityModel<Director> resource) {
       Long directorId = resource.getContent().getId();
       resource.add(linkTo(methodOn(DirectorController.class).getDirectorById(directorId)).withSelfRel());
       resource.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(directorId)).withRel("directorMovies"));
   }

   @Override
   public void addLinks(CollectionModel<EntityModel<Director>> resources) {
       resources.add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel());
     
   }
}

In this case, our entity will be wrapped in an EnityModel (this class extends RepresentationModel) to which the links specified by us in the addLinks() will be added. Here we overwrite two addLinks() methods – one for entire data collections and the other for single resources. Then, as part of the controller, it is enough to call the toModel() or toCollectionModel() method (addLinks() are template methods here), depending on whether we return a collection or a single representation.

@GetMapping
public ResponseEntity<CollectionModel<EntityModel<Director>>> getAllDirectors() {
    return ResponseEntity.ok(directorAssembler.toCollectionModel(directorService.getAllDirectors()));
}

@GetMapping(value = "directors/{id}")
public ResponseEntity<EntityModel<Director>> getDirectorById(@PathVariable("id") Long id) {
    return directorService.getDirectorById(id)
            .map(director -> {
                EntityModel<Director> directorRepresentation = directorAssembler.toModel(director)
                    .add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withRel("directors"));
                    
                return ResponseEntity.ok(directorRepresentation);
            })
            .orElse(ResponseEntity.notFound().build());
}

The main benefit of using the SimpleRepresentationModelAssembler is the separation of our entity from the RepresentationModel, as well as the separation of the adding link logic from the controller.

The problem arises when we want to add hypermedia to the nested elements of an object. Obtaining the effect, as in the example below, is impossible in a current way.

{
    "id": "M0002",
    "title": "Once Upon a Time in America",
    "year": 1984,
    "rating": "R",
    "directors": [
        {
            "id": "D0001",
            "firstname": "Sergio",
            "lastname": "Leone",
            "year": 1929,
            "_links": {
                "self": {
                    "href": "http://localhost:8080/directors/D0001"
                }
            }
        }
    ],
    "_links": {
        "self": {
            "href": "http://localhost:8080/movies/M0002"
        }
    }
}

Create DTO class with RepresentationModelAssembler

The solution to this problem is to combine the two previous methods, modifying them slightly. In our opinion, RepresentationModelAssembler offers the most possibilities. It removes the restrictions that arose in the case of nested elements for SimpleRepresentationModelAssembler. But it also requires more code from us because we need to prepare DTOs, which are often done anyway. This is the implementation based on RepresentationModelAssembler:

@Component
public class DirectorRepresentationAssembler implements RepresentationModelAssembler<Director, DirectorRepresentation> {
    @Override
    public DirectorRepresentation toModel(Director entity) {
        DirectorRepresentation directorRepresentation = DirectorRepresentation.builder()
                .id(entity.getId())
                .firstname(entity.getFirstname())
                .lastname(entity.getLastname())
                .year(entity.getYear())
                .build();

        directorRepresentation.add(linkTo(methodOn(DirectorController.class).getDirectorById(directorRepresentation.getId())).withSelfRel());
        directorRepresentation.add(linkTo(methodOn(DirectorController.class).getDirectorMovies(directorRepresentation.getId())).withRel("directorMovies"));

        return directorRepresentation;
    }

    @Override
    public CollectionModel<DirectorRepresentation> toCollectionModel(Iterable<? extends Director> entities) {
        CollectionModel<DirectorRepresentation> directorRepresentations = RepresentationModelAssembler.super.toCollectionModel(entities);

        directorRepresentations.add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withSelfRel());

        return directorRepresentations;
    }
}

When it comes to controller methods, they look the same as for SimpleRepresentationModelAssembler, the only difference is that in the ResponseEntity the return type is DTO – DirectorRepresentation.

@GetMapping
public ResponseEntity<CollectionModel<DirectorRepresentation>> getAllDirectors() {
    return ResponseEntity.ok(directorRepresentationAssembler.toCollectionModel(directorService.getAllDirectors()));
}

@GetMapping(value = "/{id}")
public ResponseEntity<DirectorRepresentation> getDirectorById(@PathVariable("id") String id) {
    return directorService.getDirectorById(id)
            .map(director -> {
                DirectorRepresentation directorRepresentation = directorRepresentationAssembler.toModel(director)
                    .add(linkTo(methodOn(DirectorController.class).getAllDirectors()).withRel("directors"));
                    
                return ResponseEntity.ok(directorRepresentation);
            })
            .orElse(ResponseEntity.notFound().build());
}

Here is our DTO model:

@Builder
@Getter
@EqualsAndHashCode(callSuper = false)
@Relation(itemRelation = "director", collectionRelation = "directors")
public class DirectorRepresentation extends RepresentationModel<DirectorRepresentation> {
   private final String id;
   private final String firstname;
   private final String lastname;
   private final int year;
}

The @Relation annotation allows you to configure the relationship names to be used in the HAL representation. Without it, the relationship names match the class name and a suffix List for the collection.

By default, JSON+HAL looks like this:

{
    "_embedded": {
        "directorRepresentationList": [
             …
         ]
    },
    "_links": {
        …
    }
}

However, annotation @Relation can change the name of directors:

{
    "_embedded": {
        "directors": [
             …
         ]
    },
    "_links": {
        …
    }
}

Summarizing the HATEOAS concept, it consists of a few pros and cons.

Pros:

  • If the client uses it, we can change the API address for our resources without breaking the client.
  • Creates good self-documentation, and table of contents of API to the person who has the first contact with our API.
  • Can simplify building some conditions on the frontend, e.g., whether the button should be disabled / enabled based on whether the link to corresponding the action exists.
  • Less coupling between frontend and backend.
  • Just like writing tests imposes on us to stick to the SRP principle in class construction, HATEOAS can keep us in check when designing API.

Cons:

  • Additional work needed on implementing non-business functionality.
  • Additional network overhead. The size of the transferred data is larger.
  • Adding links to some resources can be sometimes complicated and can introduce mess in controllers.

Changes in Spring HATEOAS 1.0

Spring HATEOAS has been available since 2012, but the first release of version 1.0 was in 2019.

The main changes concerned the changes to the package paths and names of some classes, e.g.

Old New
ResourceSupport RepresentationModel
Resource EntityModel
Resources CollectionModel
PagedResources PagedModel
ResourceAssembler RepresentationModelAssembler

It is worth paying attention to a certain naming convention – the replacement of the word Resource in class names with the word Representation. It occurred because these types do not represent resources but representations, which can be enriched with hypermedia. It is also more in the spirit of REST. We are returning the resource representations, not the resources themselves. In the new version, there is a tendency to move away from constructors in favor of static construction methods – .of().

It is also worth mentioning that the old version has no equivalent for SimpleRepresentationModelAssembler. On the other hand, the ResourceAssembler interface has only the toResource()method (equivalent – toModel()) and no equivalent for toCollectionModel(). Such a method is found in RepresentationModelAssembler and is the toModelCollection() method.

The creators of the library have also included a script that migrates old package paths and old class names to the new version. You can check it here.



Is it insightful?
Share the article!



Check related articles


Read our blog and stay informed about the industry's latest trends and solutions.


see all articles



Accelerating Data Projects with Parallel Computing


Read the article

Generative AI for Developers – Our Comparison


Read the article