Implementation of HATEOAS driven apis in Springboot

 

In Spring boot, HATEOAS stands for “Hypermedia as the engine of application state”.

Basically, it concerns with the term Hypermedia. That will be able to provide the ease access of Rest web services with the help of hypermedia driven link. Which will produce fetching or, creating CRUD operation over rest api’s.

Since, hypermedia is an important aspect of Rest api where Spring provide APIs to create/develop web application that can create Rest representations. It basically the principle of HATEOAS.

HATEOAS API’s return “application/hal+json” as a content type. That is a super set of JSON built which has supported with HATEOAS apis.

HAL (Hypertext Application Language) provide the design to represent with links. Basically, it describes how to design json representations. HAL-compliant API are basically using both links as well as embedded resources.

Following are the Benefits of using HAL:

  • Minimal as well as Lightweight of semantics,
  • It offers most of the benefits of using hypermedia type,
  • It’s Easy to convert any existing apis to Hateoas apis,
  • Identifing the resources (basically URIs),
  • Manipulation via representations ( basically for request and response body),
  • It contains self-descriptive messages (In headers)

There is an easy way to say whether an API is RESTful or not. There is an api model representation called: Richardson Maturity Model (RMM) was introduced by Leonard Richardson in 2008 QCon talk and later popularized by Martin Fowler. This model introduces four levels of API maturity starting from Level 0. So that at level two each resource is not only identified by its own URI, but all operations with resources are done using HTTP methods. Basically GET, PUT etc. If an API is at Level 3 it can be considered as RESTful. From the point of view of the RMM HAL helps to upgrade a Level 2 API to Level 3 where hypermedia is basically used.

So here the first question comes in our mind why API needs HateOas/Hypermedia APIs ?

Let’s take a look with a simple real time example: when you visit any video streaming site like: Netflix. After login it’ll present some snapshots and links to other sections of connected video links. When you click on them you’ll get more information along with more related links which are relevant to those contexts. Like: series, episodes and related video types which basically represents hateoas link representations over there so that REST client hits an initial API URI and it uses the server-provided links to dynamically discover available actions and access the resources where it needs to connect there. Basically Netflix has a REST API based on HATEOAS that includes links as part of the resources. So that, the clients no longer have to hard code the URI structures for different resources. This allows the server to make URI changes as the API evolves without breaking the clients.

 

Every REST framework provides its own way to create the HATEOAS links using framework capabilities.

To support HATEOAS, spring libarary provides maven dependency which need to be add in pom.xml file.

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-hateoas</artifactId>

<version>2.1.4.RELEASE</version>

</dependency>

Let’s take a simple project example where i’ll create two RestController classes MembershipController.java and UserController.java.

Api’s can be bind with specified urls using ServletUriComponentsBuilder class. This class having static method “fromCurrentRequest().build().toUriString()

Now I have created a hateoas database where i have populated two tables : user_info and memberships.

Note: I have added business logic and persistence logic in controller classes only for demo purpose. To explain in a brief manner as showed you to add or to fetch data only. It’s not created separate business logic class due to more no. of classes will need to add here that may be create some confusion about the main topic.

 

UserController.java


package com.code.adda.java8.rest.person;

import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.code.adda.java8.person.User;
import com.code.adda.java8.respository.person.UserRepository;
import com.code.adda.java8.respository.person.exception.UserNotFoundException;

import io.swagger.annotations.Api;

@Api(tags = “user”)
@RestController
@RequestMapping(value = “/user”, produces = “application/json”)
public class UserController {

final UserRepository userRepository;

public UserController(final UserRepository userRepository) {
this.userRepository = userRepository;
}

/**
*
* @return
*/
@GetMapping
public ResponseEntity<Resources<UserResource>> getAllUser() {
final List<UserResource> collection = userRepository.findAll().stream().map(UserResource::new)
.collect(Collectors.toList());
final Resources<UserResource> resources = new Resources<>(collection);
final String uriString = ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString();
resources.add(new Link(uriString, “self”));
return ResponseEntity.ok(resources);
}

/**
*
* @param id
* @return
*/

@GetMapping(“/user/{id}”)
public ResponseEntity<UserResource> getUserById(@PathVariable final long id) {
return userRepository.findById(id).map(p -> ResponseEntity.ok(new UserResource(p)))
.orElseThrow(() -> new UserNotFoundException(id));
}

/**
*
* @param userFromRequest
* @return
*/
@PostMapping
public ResponseEntity<UserResource> post(@RequestBody final User userFromRequest) {
final User user = userRepository.save(new User(userFromRequest));
final URI uri = MvcUriComponentsBuilder.fromController(getClass()).path(“/{id}”).buildAndExpand(user.getId())
.toUri();
return ResponseEntity.created(uri).body(new UserResource(user));
}
}

MembershipController.java


package com.code.adda.java8.rest.membership;

import java.net.URI;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resources;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.code.adda.java8.entity.membership.Membership;
import com.code.adda.java8.person.User;
import com.code.adda.java8.repository.membership.MembershipRepository;
import com.code.adda.java8.repository.membership.exception.MembershipNotFoundException;
import com.code.adda.java8.respository.person.UserRepository;
import com.code.adda.java8.respository.person.exception.UserNotFoundException;

import io.swagger.annotations.Api;

@Api(tags = “membership”)
@RestController
@RequestMapping(“/user/{userId}/memberships”)
public class MembershipController {

@Autowired
private UserRepository userRepository;

@Autowired
private MembershipRepository membershipRepository;

/**
*
* @param userId
* @return
*/
@GetMapping
public ResponseEntity<Resources<MembershipResource>> allMembership(@PathVariable final long userId) {
final List<MembershipResource> collection = getMembershipsForUser(userId);
final Resources<MembershipResource> resources = new Resources<>(collection);
final String uriString = ServletUriComponentsBuilder.fromCurrentRequest().build().toUriString();
resources.add(new Link(uriString, “self”));
return ResponseEntity.ok(resources);
}

/**
*
* @param userId
* @return
*/
private List<MembershipResource> getMembershipsForUser(final long userId) {
return userRepository.findById(userId)
.map(u -> u.getMemberships().stream().map(MembershipResource::new).collect(Collectors.toList()))
.orElseThrow(() -> new UserNotFoundException(userId));
}

/**
*
* @param userId
* @param membershipId
* @return
*/
@GetMapping(“/{membershipId}”)
public ResponseEntity<MembershipResource> getMembershipFromId(@PathVariable final long userId,
@PathVariable final long membershipId) {
return userRepository.findById(userId)
.map(p -> p.getMemberships().stream().filter(m -> m.getId().equals(membershipId)).findAny()
.map(m -> ResponseEntity.ok(new MembershipResource(m)))
.orElseThrow(() -> new MembershipNotFoundException(membershipId)))
.orElseThrow(() -> new UserNotFoundException(userId));
}

/**
*
* @param userId
* @param inputMembership
* @return
*/
@PostMapping
public ResponseEntity<MembershipResource> post(@PathVariable final long userId, @RequestBody final Membership membership) {
return userRepository.findById(userId).map(u -> {
final Membership mem = saveMembershipDetails(u, membership);
final URI uri = createPostUri(mem);
return ResponseEntity.created(uri).body(new MembershipResource(mem));
}).orElseThrow(() -> new UserNotFoundException(userId));
}

/**
*
* @param person
* @param membership
* @return
*/
private Membership saveMembershipDetails(final User person, final Membership membership) {
return membershipRepository.save(new Membership(person, membership.getName(), membership.getCost()));
}

/**
*
* @param membership
* @return
*/
private URI createPostUri(final Membership membership) {
return MvcUriComponentsBuilder.fromController(getClass()).path(“/{membershipId}”)
.buildAndExpand(membership.getOwner().getId(), membership.getId()).toUri();
}
}

How To Run The Application:

To run the application, i have implemented swagger to show app apis endpoints, attached below.

 

Now I’ll hit one url from here and show the HATEOAS generated output:

Url: http://localhost:8050/user/{userId}/memberships/{membershipId}

Inputs: userId=1, membershipId=1

Output:

{
“membership”: {
“id”: 1,
“name”: “Library”,
“cost”: 1000
},
“_links”: {
“membership-id”: {
“href”: “1”
},
“memberships”: {
“href”: “http://localhost:8050/user/1/memberships”
},
“owner”: {
“href”: “http://localhost:8050/user/user/1”
},
“self”: {
“href”: “http://localhost:8050/user/1/memberships/1”
}
}
}

 

Url: http://localhost:8050/user/user/{userId}

Inputs: userId=2

Output:

{
“user“: {

id“: 2,
“firstName“: “BBB”,
“secondName“: “BB2”,
“dateOfBirth“: “28 / 06 / 1999 20: 58”,
“profession“: “Intern”,
“salary“: 15000
},
“_links“: {
“user“: {
“href“: “http: //localhost:8050/user”

},

“memberships“: {

“href“: “http://localhost:8050/user/2/memberships”

},

“self“: {

“href“: “http://localhost:8050/user/user/2”

}

}

}

 

Now we can see that when we hit any api url and which is concerned with some other related apis where i can find the expected output based on their requirement hateoas will provide the convenient way to link those api urls which has need for further concern.

Note: Every HATEOAS api contain one “self“: link which will represent itself i.e: The URI which is associated with the response is qualified with self link. which is simply the canonical location the Resource can be accessed with itself..

In this pragmatic approach assured that us the cautious in creating full-fledged hypermedia API in real world implementation.

 

 

2 Comments

Leave a Reply