Appearance
HATEOAS: Links in REST Responses
Chapters 18 and 19 acted on requests on their way in. This chapter adds something to the response: links. The path: what HATEOAS is, with an example; why you would use it; why you should not add every possible link; how to find the one place a link really helps; and how to add links in Spring Boot.
HATEOAS stands for Hypermedia As The Engine Of Application State. The idea: the response tells the client what it can do next with the item it just received.
An example
A client adds a user with POST /api/adduser. The user is created but unverified. From here the client could:
GETthe user's details, orDELETEthe user- verify the user, by SMS (
/sms-verify-start, then/sms-verify-finishwith the PIN received) or by email (/email-verify-start, then/email-verify-finish)
Without HATEOAS, the response is plain data:
json
{
"userID": "123456",
"name": "John",
"verifyStatus": "UNVERIFIED"
}With HATEOAS, it also carries links to the next actions:
json
{
"userID": "123456",
"name": "John",
"verifyStatus": "UNVERIFIED",
"links": [
{ "rel": "self", "href": "/api/user/123456", "type": "GET" }
]
}Each link has:
href: the URI to call nextrel: how that URI relates to this item.selfmeans "this item itself", which in practice is the GET call for its details.type: here used for the HTTP method (GET, POST, PATCH, DELETE)
Why use it
Two purposes:
- Loose coupling between client and server.
- API discovery: the server may have hundreds of APIs. The client does not need to know which one applies now; the response tells it.
The server puts the next actions in the response, so the client carries less business logic about which API to call, when, and how.
Why not to add every action
Listing everything possible after adduser (get, delete, update, sms-verify-start, sms-verify-finish, ...) bloats the response. These links are dynamic: which actions apply depends on the current state, so the server computes them on each request. That means:
- more complexity on the server
- more latency
- bigger payloads
This is why HATEOAS gets a bad name. Never add all possible next actions just because you can. Analyse where the client and server are actually tightly coupled, and add links only there.
Where the coupling is
In the example, the coupling is in verification. Suppose that without HATEOAS the server sends:
json
{
"userID": "123456",
"name": "John",
"verifyStatus": "UNVERIFIED",
"verifyType": "SMS",
"verifyState": "NOT_YET_STARTED"
}The client must now contain this logic to pick the right API:
java
if (verifyStatus.equals("UNVERIFIED")) {
if (verifyType.equals("SMS")) {
if (verifyState.equals("NOT_YET_STARTED")) {
// call POST /sms-verify-start
} else if (verifyState.equals("STARTED")) {
// call POST /sms-verify-finish
}
} else if (verifyType.equals("EMAIL")) {
if (verifyState.equals("NOT_YET_STARTED")) {
// call POST /email-verify-start
} else if (verifyState.equals("STARTED")) {
// call POST /email-verify-finish
}
}
}That is business logic, and it belongs on the server. With one HATEOAS link, the server does the deciding. Say SMS verification is already started, so the only valid next step is finish:
json
{
"userID": "123456",
"name": "John",
"verifyStatus": "UNVERIFIED",
"links": [
{ "rel": "verify", "href": "/api/sms-verify-finish/123456", "type": "POST" }
]
}The client shrinks to:
java
if (verifyStatus.equals("UNVERIFIED")) {
// call the "verify" link from the response
}One link, in the one place where it removes coupling.
Adding links in Spring Boot
The dependency (the Spring Boot parent picks the version):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>Many responses may need links, so put the list in one parent class and let those responses extend it:
java
public class HateoasLinks {
private List<Link> links = new ArrayList<>();
public void addLink(Link link) {
links.add(link);
}
public List<Link> getLinks() {
return links;
}
}java
public class UserResponse extends HateoasLinks {
private String userID;
private String name;
private String verifyStatus;
// getters and setters
}Link comes from the Spring HATEOAS dependency. Keep the getLinks() getter: Jackson writes only fields it can read, so without it the links silently disappear from the JSON.
java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
User user;
@PostMapping("/adduser")
public ResponseEntity<UserResponse> addUser() {
UserResponse response = user.getUser();
// business logic decides which verify API is next
Link verifyLink = WebMvcLinkBuilder.linkTo(UserController.class)
.slash("sms-verify-finish")
.slash(response.getUserID())
.withRel("verify")
.withType("POST");
response.addLink(verifyLink);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}linkTo(UserController.class) starts from the controller's own mapping, so the URI follows the code if the mapping changes. The link is absolute:
json
{
"links": [
{ "rel": "verify", "href": "http://localhost:8080/api/sms-verify-finish/123456", "type": "POST" }
],
"userID": "123456",
"name": "John",
"verifyStatus": "UNVERIFIED"
}The other way is to write the URI yourself:
java
Link verifyLink = Link.of("/api/sms-verify-finish/" + response.getUserID())
.withRel("verify")
.withType("POST");Two notes:
- Strictly, a link's
typeis meant for a media type (such asapplication/json), not the HTTP method. Many teams use it for the method anyway, as here. Agree on it with the client team. - Spring HATEOAS already has a parent class like
HateoasLinks:RepresentationModel. A response that extends it and callsadd(link)is written in the HAL format, with links grouped byrelunder"_links":"_links": {"verify": {"href": "...", "type": "POST"}}.
Next: A response has more than a body: a status code and headers too. Chapter 21 covers ResponseEntity, which controls all three, and the HTTP status codes from 1xx to 5xx: which one to return when.
Check yourself
- What does HATEOAS stand for, and what does it tell the client?
- What are the two purposes of using HATEOAS links?
- What do
href,relandtypemean in a link? What doesrel: selfusually mean? - Give three disadvantages of adding every possible next action.
- In the verification example, what logic moved from the client to the server?
- Why does
HateoasLinksneed agetLinks()method? - What are the two ways to create a
Linkshown here? - Which Spring HATEOAS class already plays the role of
HateoasLinks, and how does its JSON differ?
Answers
- Hypermedia As The Engine Of Application State. It tells the client which actions it can take next on the item.
- Loose coupling between client and server, and API discovery.
href: the URI to call.rel: its relation to the current item.type: here, the HTTP method.self: the item itself, usually the GET for its details.- More server complexity (links are computed per state), more latency, bigger payloads.
- Deciding between SMS and email, and between start and finish, based on the verify type and state.
- Jackson serializes only what it can read; without a getter,
linksis not in the response. WebMvcLinkBuilder.linkTo(Controller.class).slash(...)..., orLink.of("uri"), each followed bywithRelandwithType.RepresentationModel. It uses HAL: links go under"_links", keyed byrel.