Enterprise Java

RESTful services with HATEOAS: REST APIs and Hypermedia on JVM

1. Introduction

So far we have spent a fair amount of time talking about the role of the hypermedia and HATEOAS in the RESTful web services and APIs, glancing over different specifications and usability aspects. It sounded like supporting hypermedia and HATEOAS is not that difficult, just pick you favorites and you are good to go! As you may guess, the reality is “well, it depends” and along this part of the tutorial we are going to understand “why”. The path to learn the things the hard way is to design and implement them from scratch. This is exactly what we are going to be busy with, bringing to live the RESTful web APIs and clients on the JVM platform, primarily in Java.

The application we are going to design RESTful web APIs for is an extended version of the case study we have touched upon before. From the business perspective, we are going to implement a car rentals platform were customers could rent a car. Our goal is to build this platform with accordance to the REST architectural style principles and constraints.

2. From CRUD to Workflow

The presence of the hypermedia as a driving force dramatically changes the design process. The unfortunate truth is that the majority of the HTTP-based web services and APIs out there are essentially CRUD proxies for data stores behind them. Just exposing the data model would not help the clients to understand what they could do with it, they are doomed to replicate the server’s business logic (or at least, large part of it) in many places. This is not what well-thought REST applications should be doing.

A REST API should spend almost all of its descriptive effort in defining the media type(s) used for representing resources and driving application state, or in defining extended relation names and/or hypertext-enabled mark-up for existing standard media types. Any effort spent describing what methods to use on what URIs of interest should be entirely defined within the scope of the processing rules for a media type (and, in most cases, already defined by existing media types).

https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven

Instead of CRUD, we should think about our RESTful web APIs as workflows. By agreeing on appropriate media types, the server and client share the common understanding how to interpret them. The presence of links, relations and actions (affordances) guides the clients towards possible next steps to take. In other words, services define and share the workflows for clients to follow.

3. On the Server

Before we dig into libraries and frameworks, it would be great to understand what is the workflow of the RESTful web APIs we are trying to build as part of the application. The picture below is an attempt to do so.

REST APIs on jvm - Car Rentals API Workflow
Car Rentals API Workflow

Admittedly, the workflow is far from being exhaustive and complete however it is sufficiently good for illustrating the complexities, challenges and benefits of the real-world RESTful web APIs.

And last but not least, we have not decided yet what hypermedia specification we are going to use. The simplicity, lightweight structure and widespread adoption of HAL (along with HAL-FORMS) makes it a good choice in most cases and this is what we are going to use for our RESTful web APIs.

With the strategic decisions being made, it is time to discuss the technical details. Primarily, we are looking for library or framework which would help us.

3.1. JAX-RS

The Jakarta RESTful Web Services specification, better known as JAX-RS 2.1 (JSR-370), is one of the most popular choices for implementing RESTful web services and APIs on JVM platform.

JAX-RS: Java API for RESTful Web Services (JAX-RS) is a Java programming language API spec that provides support in creating web services according to the Representational State Transfer (REST) architectural pattern.

https://projects.eclipse.org/projects/ee4j.jaxrs

Although it includes comprehensive server-side and client-side support, it barely tackles any hypermedia capabilities besides introducing a very basic Link representation.

Some JAX-RS reference implementations, like for example Jersey, made an effort  to go ahead and bundle own proprietary extensions to facilitate hypermedia support however as you may expect, these are not part of the specification. It is certainly helpful but still requires large chunk of work to be done by developers.

3.2. Quarkus, Micronaut, Helidon,  …

The accelerated shift towards microservices architecture and cloud computing caused the proliferation of the new generation of the cloud-native frameworks. This is particularly true for JVM platform, where the well-established leaders are being challenged by Quarkus, Micronaut and Helidon, to name a few.

As the matter of fact, hypermedia and HATEOAS is not the first priority for most of them. Micronaut is an example of the outlier which at least includes basic hypermedia elements, but by and large, you are on your own.

3.3. Crnk

If you happen to select JSON:API specification to power your RESTful web services and APIs, the Crnk framework is certainly worth looking at.

Crnk is an implementation of the JSON API specification and recommendations in Java to facilitate building RESTful applications. It provides many conventions and building blocks that application can benefit from. This includes features such as sorting, filtering, pagination, requesting complex object graphs, sparse field sets, attaching links to data or atomically execute multiple operations. Further integration with frameworks and libraries such as Spring, CDI, JPA, Bean Validation, Dropwizard, Servlet API, Zipkin and and more ensure that JSON API plays well together with the Java ecosystem.

https://github.com/crnk-project/crnk-framework

Crnk takes a resource-centric approach to API modeling which basically leads to pretty clean and maintainable implementation. To quote the documentation, resources, relationships and repositories are the central building blocks of Crnk. The code snippets below illustrate these concepts pretty well.

@JsonApiResource(type = "customer", resourcePath = "customers")
public class Customer {
    @JsonApiId private String id;
    @NotNull @NotBlank private String firstName;
    @NotNull @NotBlank private String lastName;
    @JsonApiRelation(mappedBy = "customer")
    private Collection<Reservation> reservations;

    // Getters and setters here
    ...
}

@JsonApiResource(type = "reservation", resourcePath = "reservations")
public class Reservation {
    @JsonApiId private String id;
    private String vehicle;
    @NotNull @FutureOrPresent private LocalDate from;
    @NotNull @FutureOrPresent private LocalDate to;
    @JsonApiRelation private Customer customer;

    // Getters and setters here
    ...
}

@Repository
public class CustomerRepository implements ResourceRepository<Customer, String> {
    // Implementation of the repository methods here
    ...
}

@Repository
public class ReservationRepository implements ResourceRepository<Reservation, String>  {
    // Implementation of the repository methods here
    ...
}

And that is basically it, depending on the integration (Vert.X, JAX-RS and Spring Boot), the Crnk framework takes care of the rest for you. Unfortunately, Crnk does not support ALPS or JSON Hyper-Schema (at least, out of the box).

But since we decided to use HAL, not JSON:API, we have to continue our search.

3.4. Spring HATEAOS

The amount of projects which constitute Spring portfolio is really impressive. The one we have a particular interest in is Spring HATEOAS, a library to support implementing representations for hypermedia driven RESTful web services and APIs. It implements HAL, HAL-FORMS, Collection+JSON and UBER specifications and, bonus point, comes with ALPS support, the perfect fit for us to accomplish the goals we have set.

As one may expect, Spring HATEOAS integrates naturally with typical Spring Boot web applications, including both traditional Spring MVC and reactive Spring WebFlux stacks. The @EnableHypermediaSupport annotation along with Spring Boot auto-configuration capabilities activates the hypermedia support according to one (or more) specifications of your choice.

@SpringBootConfiguration
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
public class ReservationServiceConfig {
    @Bean
    HalFormsConfiguration halFormsConfiguration() {
        final HalFormsConfiguration configuration = new HalFormsConfiguration();
        configuration.registerPattern(LocalDate.class, "yyyy-MM-dd");
        return configuration;
    }
}

The entry point to our RESTful web APIs is going to be served by RootController, following unofficial naming convention.

@RestController
public class RootController {
    @GetMapping("/")
    public ResponseEntity<RepresentationModel<?>> root() {
        final RepresentationModel<?> model = new RepresentationModel<>();
        model.add(linkTo(methodOn(RootController.class).root()).withSelfRel());
        model.add(templated(linkTo(methodOn(ReservationController.class).findAll(null)), "reservations")
            .withProfile(linkTo(methodOn(RootController.class).reservations()).withSelfRel().getHref()));
        model.add(linkTo(methodOn(CustomerController.class).findAll())
            .withRel("customers")
            .withProfile(linkTo(methodOn(RootController.class).customers()).withSelfRel().getHref()));
        return ResponseEntity.ok(model);
    }
}

The HAL document returned by the controller hints about next available navigation directions.

{                                                                         
  "_links": {                                                            
    "self": {                                                            
      "href": "https://rentals.jcg.com"                                   
    },                                                                    
    "reservations": {                                                    
      "href": "https://rentals.jcg.com/reservations{?page,size,sort}",     
      "profile": "https://rentals.jcg.com/alps/reservations",              
      "templated" : true                                                  
    },                                                                    
    "customers": {                                                       
      "href": "https://rentals.jcg.com/customers",                         
      "profile": "https://rentals.jcg.com/alps/customers"                  
    }                                                                     
  }                                                                       
}

There are a few things that may have caught your eye. The first is the link to reservations relation which is returned as a template. The second one is the presence of profile attribute for each link relation, pointing to respective ALPS profile. The code snippet below illustrates the Spring HATEOAS APIs for constructing ALPS profile for reservations collection resource.

@GetMapping(value = "/alps/reservations", produces = MediaTypes.ALPS_JSON_VALUE)
public ResponseEntity<Alps> reservations() {
   return ResponseEntity.ok(Alps
       .alps()
       .doc(doc()
           .href("https://rentals.jcg.com/documentation.html")
           .build())
       .descriptor(List.of(
           descriptor()
               .id("reservations")
               .type(Type.SEMANTIC)
               .rt("#reservation")
               .descriptor(Arrays.asList(
                   descriptor()
                       .id("book")
                       .name("reservations")
                       .type(Type.UNSAFE)
                       .rt("#reservation")
                       .build(),
                   descriptor()
                       .id("list")
                       .name("reservations")
                       .type(Type.SAFE)
                       .rt("#reservation")
                       .build()
                   ))
               .build(),
           descriptor()
               .id("reservation")
               .type(Type.SEMANTIC)
               .descriptor(Stream
                   .concat(
                       PropertyUtils
                           .getExposedProperties(Reservation.class)
                           .stream()
                           .map(property -> descriptor()
                               .id(property.getName())
                               .href(href(property)) 
                               .type(Type.SEMANTIC)
                               .build()),
                       Stream.of(
                            descriptor()
                               .id("customer")
                               .type(Type.SAFE)
                               .rt("#customer")
                               .build(),
                           descriptor()
                               .id("update")
                               .name("reservation")
                               .type(Type.IDEMPOTENT)
                               .rt("#reservation")
                               .build(),
                           descriptor()
                               .id("cancel")
                               .name("reservation")
                               .type(Type.IDEMPOTENT)
                               .rt("#reservation")
                               .build()))
                   .collect(Collectors.toList()))
               .build()))
       .build());
}

Respectively, here is the JSON representation of the ALPS reservations collection resource profile, which basically is what the clients are going to deal with.

{
  "version": "1.0",
  "doc": {
    "href": "https://rentals.jcg.com/documentation.html"
  },
  "descriptor": [ {
    "id": "reservations",
    "type": "SEMANTIC",
    "descriptor": [ {
      "id": "book",
      "name": "reservations",
      "type": "UNSAFE",
      "rt": "#reservation"
    }, {
      "id": "list",
      "name": "reservations",
      "type": "SAFE",
      "rt": "#reservation"
    } ],
    "rt": "#reservation"
  }, {
    "id": "reservation",
    "type": "SEMANTIC",
    "descriptor": [ {
      "id": "from",
      "href" : "https://schema.org/Date",
      "type": "SEMANTIC"
    }, {
      "id": "id",
      "href" : "https://schema.org/Thing#identifier",
      "type": "SEMANTIC"
    }, {
      "id": "to",
      "href" : "https://schema.org/Date",
      "type": "SEMANTIC"
    }, {
      "id": "vehicle",
      "href" : "https://schema.org/Vehicle#name",
      "type": "SEMANTIC"
    }, {
      "id": "customer",
      "type": "SAFE",
      "rt": "#customer"
    }, {
      "id": "update",
      "name": "reservation",
      "type": "IDEMPOTENT",
      "rt": "#reservation"
    }, {
      "id": "cancel",
      "name": "reservation",
      "type": "IDEMPOTENT",
      "rt": "#reservation"
    } ]
  } ]
}

In spirit of hypermedia and HATEOAS, the Spring HATEOAS approach is also resource-oriented (or better to say, resource representation oriented). Basically, you have to implement a number of RepresentationModelAssemblers (for example, ReservationResourceAssembler) and controller endpoints which rely on respective assemblers to construct individual resource representations or resource collection representations.

@Component
public class ReservationResourceAssembler implements SimpleRepresentationModelAssembler<Reservation> {
    @Override
    public void addLinks(EntityModel<Reservation> resource) {
        resource.add(linkTo(methodOn(CustomerController.class).findOne(resource.getContent().getCustomerId()))
            .withRel("customer")
            .withType(linkTo(methodOn(RootController.class).customers()).slash("#customer").toString()));
        resource.add(linkTo(methodOn(ReservationController.class).findOne(resource.getContent().getId()))
            .withSelfRel()
            .withType(linkTo(methodOn(RootController.class).reservations()).slash("#reservation").toString())
            .andAffordance(afford(methodOn(ReservationController.class).modify(resource.getContent().getId(), null)))
            .andAffordance(afford(methodOn(ReservationController.class).cancel(resource.getContent().getId()))));
    }
}

Alongside the link relation to customer, there is a number of affordances (actions) designated to alter the reservation resource state (modify or cancel it). Also, because reservations collection resource is using paging (and sorting), the construction of its representation is a little bit more complicated and involves two assemblers, let us take a look at the example.

@RestController
@RequestMapping(path = "/reservations")
public class ReservationController {
    @Autowired private ReservationRepository repository;
    @Autowired private ReservationResourceAssembler reservationResourceAssembler;
    @Autowired private PagedResourcesAssembler>Reservation< assembler;
    
    @GetMapping
    public ResponseEntity>PagedModel>EntityModel>Reservation<<< findAll(@PageableDefault Pageable pageable) {
        return ResponseEntity.ok(assembler.toModel(repository.findAll(pageable), reservationResourceAssembler));
    }
}

To demonstrate the effect of the paging, it is sufficient to fetch the reservations collection with the page size of let say 2 elements.

{                                                                                        
  "_embedded": {                                                                        
    "reservations": [ {                                                                 
      "id": "13e1892765c5",                                                             
      "vehicle": "Honda Civic 2020",                                                    
      "from": "2020-01-01",                                                             
      "to": "2020-01-05",                                                               
      "_links": {                                                                       
        "customer": {                                                                   
          "href": "https://rentals.jcg.com/customers/fed195a03e9d",                       
          "type": "https://rentals.jcg.com/alps/customers#customer"                       
        },                                                                               
        "self": {                                                                       
          "href": "https://rentals.jcg.com/reservations/13e1892765c5",                    
          "type": "https://rentals.jcg.com/alps/reservations#reservation"                 
        }                                                                                
      },                                                                                 
      "_templates": {                                                                   
        "cancel": {                                                                     
          "method": "delete",                                                           
          "properties": [ ]                                                             
        },                                                                               
        "default": {                                                                    
          "method": "put",                                                              
          "properties": [ {                                                             
            "name": "from",                                                             
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "to",                                                               
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "vehicle",                                                          
            "required": true                                                            
          } ]                                                                            
        }                                                                                
      }                                                                                  
    }, {                                                                                 
      "id": "fc14e8ef90f5",                                                             
      "vehicle": "BMW 325i",                                                            
      "from": "2020-01-10",                                                             
      "to": "2020-01-12",                                                               
      "_links": {                                                                       
        "customer": {                                                                   
          "href": "https://rentals.jcg.com/customers/fed195a03e9d",                       
          "type": "https://rentals.jcg.com/alps/customers#customer"                       
        },                                                                               
        "self": {                                                                       
          "href": "https://rentals.jcg.com/reservations/fc14e8ef90f5",                    
          "type": "https://rentals.jcg.com/alps/reservations#reservation"                 
        }                                                                                
      },                                                                                 
      "_templates": {                                                                   
        "cancel": {                                                                     
          "method": "delete",                                                           
          "properties": [ ]                                                             
        },                                                                               
        "default": {                                                                    
          "method": "put",                                                              
          "properties": [ {                                                             
            "name": "from",                                                             
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "to",                                                               
            "regex": "yyyy-MM-dd",                                                      
            "required": true                                                            
          }, {                                                                           
            "name": "vehicle",                                                          
            "required": true                                                            
          } ]                                                                            
        }                                                                                
      }                                                                                  
    } ]                                                                                  
  },                                                                                     
  "_links": {                                                                           
    "first": {                                                                          
      "href": "https://rentals.jcg.com/reservations?page=0&size=2"                        
    },                                                                                   
    "self": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=0&size=2"                        
    },                                                                                   
    "next": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=1&size=2"                        
    },                                                                                   
    "last": {                                                                           
      "href": "https://rentals.jcg.com/reservations?page=1&size=2"                        
    }                                                                                    
  },                                                                                     
  "page": {                                                                             
    "size": 2,                                                                          
    "totalElements": 3,                                                                 
    "totalPages": 2,                                                                    
    "number": 0                                                                         
  }                                                                                      
}

It is easy to spot the additional navigation links first, next and last which are in fact context dependent (for example, since we asked for the first page, the prev link relation is not present).

It won’t be exaggeration to claim that Spring HATEOAS offers the most comprehensive hypermedia and HATEOAS  support for JVM platform. Although it does not implement some of the specification out of the box, it allows to plug custom media types through a set of SPIs.

3.5. Hydra-Java

The RESTful web services and APIs which decided to adopt JSON-LD and Hydra might benefit from using hydra-java library. The presence of the extension for Spring HATEAOS is very encouraging but unfortunately it is shaded by the fact that it does not work with the latest Spring HATEOAS versions.

With that, we have a pretty good idea regarding server-side implementation of the RESTful web services and APIs, it is time to switch the subject and talk about the client-side.

4. On the Client

From the client-side perspective, it makes sense to distinguish two perspectives or classes of hypermedia API clients:

  • hypermedia API client in the context of (web) user interface (frontend)
  • hypermedia API client in the context of business task implementation (backend)

Whereas JavaScript is the number one choice for web frontend development, Java (and JVM in general) has established the dominant positions on the backend side. Although we are going to talk more about the latter many concepts would equally apply to both.

So what are the principles behind design and implementation of the hypermedia API clients? If there is one thing to highlight, it is to focus on programming against hypermedia specification, not by inspecting the responses from the server. The media type should give the clients all the necessary details and serve as the implementation guidance. Also, the clients may just use some specific flows and there is no need to implement everything the service has to offer.

4.1. JAX-RS

The JAX-RS 2.1 specification includes the client part which, unfortunately, only provides the way to extract the links from the Link header.

final Client client = ClientBuilder
    .newClient();

try (final Response response = client
        .target("https://rentals.jcg.com/")
        .request()
        .accept("application/prs.hal-forms+json")
        .get()) {
                    
    final Link customers = response.getLink("customers");
    if (customers != null) {
        // follow the link here 
    }
} finally {
    client.close();
}

Basically, as with the server-side, if you need to have things done, be ready to roll the sleeves.

4.2. Crnk

The Crnk framework comes with a pretty good client support implemented with the familiar building blocks: resources, relationships and repositories.

final CrnkClient client = new CrnkClient("https://rentals.jcg.com/");
client.setHttpAdapter(new OkHttpAdapter());
        
final ResourceRepository>Customer, String< repository = client.getRepositoryForType(Customer.class);

final ResourceRepository>Customer, String< repository = client.getRepositoryForType(Customer.class);
final List>Customer< customers = repository.findAll(
    new QuerySpec(Customer.class)
        .setPaging(new OffsetLimitPagingSpec(0L, 10L)));

if (!customers.isEmpty()) {
    // navigate through customers
}

If your RESTful web services and APIs follow the JSON:API specification, the Crnk client could save you a lot of time and effort.

4.3. Spring HATEAOS

Surprisingly, up to recently Spring HATEOAS had somewhat incomplete support for hypermedia clients but the latest releases have brought a number of improvements. The Traverson is the oldest mechanism supported by Spring HATEOAS for navigating through links and relations.

final RestTemplate template = ...;
            
final Map>String, Object< paging = Map.of(
        "page", 0L,
        "size", 2L
    );
    
final CollectionModelType>Reservation< resourceType =
    new TypeReferences.CollectionModelType>Reservation<() {};
            
final Traverson traverson = new Traverson(URI.create("https://rentals.jcg.com/"), MediaTypes.HAL_FORMS_JSON)
    .setLinkDiscoverers(List.of(new HalFormsLinkDiscoverer()))
    .setRestOperations(template);
            
final CollectionModel>Reservation< reservations = traverson
    .follow(rel("reservations").withParameters(paging))
    .toObject(resourceType);;
                
if (!reservations.getContent().isEmpty()) {
    // navigate through reservations
}

More traditional general-purpose API clients (let us call them just REST clients) like WebClient and RestTemplate have been augmented with hypermedia support as well.

final WebClient client = builder.build();
                
final CollectionModelType>Reservation< resourceType =
    new TypeReferences.CollectionModelType>Reservation<() {};
final LinkDiscoverer discoverer = new HalFormsLinkDiscoverer();
    
final Optional>Link< link = client
    .get()
    .uri("https://rentals.jcg.com/")
    .accept(MediaTypes.HAL_FORMS_JSON)
    .retrieve()
    .bodyToMono(String.class)
    .map(r -< discoverer.findLinkWithRel("reservations", r))
    .block();
            
if (link.isPresent()) {
    final Map>String, Object< paging = Map.of(
            "page", 0L,
            "size", 2L
        );

    final URI uri = link
        .get()
        .getTemplate()
        .expand(paging);
                
     final CollectionModel>Reservation< reservations = client
        .get()
        .uri(uri)
        .accept(MediaTypes.HAL_FORMS_JSON)
        .retrieve()
        .bodyToMono(resourceType)
        .block();
                                
    if (!reservations.getContent().isEmpty()) {
        // navigate through reservations
    }
}

Ether you pick the Traverson, RestTemplate or WebClient, you would certainly be able to implement the powerful hypermedia API clients to fully automate the service workflows.

5. Conclusions

In this part of the tutorial we have talked about libraries and frameworks which could help you to design and implement hypermedia-driven RESTful web APIs and their clients on JVM platform. Although there aren’t too many, you have the options to choose from.

We have skipped over the discussion about hypermedia API clients in the context of web frontends since it is primarily done on the browser side using JavaScript. In this regards, it is still worth mentioning Traverson, a hypermedia API/HATEOAS client for Node.js and the browser (yes, this is the library Spring HATEAOS took some inspiration from).

6. What’s next

In the next, the final part of the tutorial we are going to summarize the theory and practice behind the most forgotten and mysterious constraint of the REST architectural style, hypermedia as the engine of application state (HATEOAS).

7. Download the source code

Download
You can download the full source code of this example here: RESTful services with HATEOAS: REST APIs and Hypermedia on JVM

Andrey Redko

Andriy is a well-grounded software developer with more then 12 years of practical experience using Java/EE, C#/.NET, C++, Groovy, Ruby, functional programming (Scala), databases (MySQL, PostgreSQL, Oracle) and NoSQL solutions (MongoDB, Redis).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button