Kotlin and Spring Boot: Hypermedia Driven Web Service
Learn about HATEOAS, build a state machine to model an article review workflow, use Spring-HATEOAS and see how hypermedia clients adapt. By Prashant Barahi.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin and Spring Boot: Hypermedia Driven Web Service
30 mins
- Getting Started
- What Is a State Machine?
- Three Level Review Workflow
- Building the 3LRW State Machine
- Handling Events
- Understanding Hypermedia Responses
- Supporting Hypermedia in Spring Boot
- Media Types
- Building the “Self” Link
- Affordance
- Building the “update” Link
- Building the Tasks Link
- Understanding the Hypermedia Client
- A Non-Hypermedia-Based Client
- Four Level Review Workflow
- Building the 4LRW State Machine
- Where to Go From Here?
Handling Events
To handle the events, open ArticleService.kt and look at the following method:
fun handleEvent(articleId: Long, event: ArticleEvent) {
val article = repository.findById(articleId).orElseThrow()
// 1
val stateMachineFactory = stateMachineFactoryProvider
.getDefaultStateMachineFactory<ArticleState, ArticleEvent>()
// 2
val stateMachine = stateMachineFactory
.buildFromHistory(article.getPastEvents())
// 3
stateMachine.setOnTransitionListener(object :
OnStateTransitionListener<ArticleState, ArticleEvent> {
override fun onTransition(
prevState: ArticleState,
event: ArticleEvent,
nextState: ArticleState
) {
article.state = nextState
article.consumeEvent(event)
}
})
// 4
val eventResult = stateMachine.sendEvent(event)
if (!eventResult) {
throw StaleStateException("Event ${event.alias} could not be accepted.")
}
repository.save(article)
}
Here’s what’s going on:
-
You get the default
StateMachineFactory
, which is the one whose transitions you reconfigured before. -
On this factory, you could call
create()
to get a fresh instance of the 3LRWStateMachine
. But in this case, you need to restore aStateMachine
to the article’s current state by callingbuildFromHistory()
and passing in the list of events it must consume to reach that state. This is called Event Sourcing. - Whenever a transition happens, you update the state of the article and store the corresponding event.
-
You call
sendEvent()
to initiate a state transition. All the events that have been consumed successfully get persisted to the database (and are used for Event Sourcing).
Take a few moments to explore this setup. In the next section, you’ll learn about HATEOAS.
Understanding Hypermedia Responses
Consider a hypothetical endpoint that serves articles. Sending a GET
request to /articles/1
should return an article with ID of 1.
{
"id": 1,
"title": "Heading 1",
"body": "Lorem ipsum.",
"status": "DRAFT"
}
It’s difficult to know what actions are allowed on a resource, just from the response above. However, a hypermedia response includes actions that are currently available as well. The server crafts them using the current snapshot of the resource.
So what does this “hypermedia response” look like? A GET /articles/1
but on a hypermedia-based server returns this:
{
"id": 1,
"title": "Heading 1",
"body": "Lorem ipsum.",
"status": "DRAFT",
"_links": {
"self": {
"href": "http://localhost:8080/articles/1"
},
"update": {
"href": "http://localhost:8080/articles/1"
}
}
}
Through this response, a client can determine what operations are allowed on a resource. The presence of _links.update
implies it’s possible to update that resource and use that information to, for example, display an Update button to the users.
Next, you’ll learn to build these hypermedia links.
Supporting Hypermedia in Spring Boot
The sample project uses the Spring-HATEOAS library to construct the hypermedia response. You’ll notice the following dependency in the build.gradle.kts:
implementation("org.springframework.boot:spring-boot-starter-hateoas")
This library provides static methods under WebMvcLinkBuilder
to introspect methods. It also has classes like RepresentationModel
, CollectionModel
and so on to model the hypermedia response.
You enable the support for hypermedia by annotating the Application
using @EnableHypermediaSupport
(the project is already annotated with it):
import org.springframework.hateoas.config.EnableHypermediaSupport
@SpringBootApplication
@EnableHypermediaSupport(type = [
EnableHypermediaSupport.HypermediaType.HAL,
EnableHypermediaSupport.HypermediaType.HAL_FORMS,
])
class Application
Later, you’ll learn what these HypermediaType.HAL
and HypermediaType.HAL_FORMS
are. For now, go to ArticleResource.kt, where you’ll find ArticleResource
inheriting from RepresentationModel
. ArticleController
exposes the /articles
endpoint that returns an ArticleResource
or a variation of it.
Finally, go to the most important class: ArticleAssembler.kt. Here, you craft the hypermedia response the client receives. Its outline looks like this:
@Component
class ArticleAssembler
@Autowired constructor(
private val stateMachineFactoryProvider: StateMachineFactoryProvider
) : RepresentationModelAssembler<ArticleEntity, ArticleResource> {
// ...
}
The ArticleAssembler
translates the database entity ArticleEntity
to the ArticleResource
.
Build and run the server and execute this curl command:
curl http://localhost:8080/articles -H "Accept:application/hal+json"
And fetch the article with ID 1 using:
curl http://localhost:8080/articles/1 -H "Accept:application/hal+json"
The responses you get aren’t any different than the non-hypermedia-based responses. Next, you’ll learn what this application/hal+json
is!
Media Types
In the curl commands you executed before, you sent a strange looking Accept
header: application/hal+json
. This is the registered media type identifier for JSON HAL (JSON Hypertext Application Language). HAL is the simplest and most widely adopted hypermedia media type.
Another media type called HAL-FORMS provides information on HTTP methods, message content-type and request parameters to use when making a request to the server. Its associated media type identifier is application/prs.hal-forms+json
.
Other media types are available for hypermedia as well, and the “On choosing a hypermedia type for your API” blog does a great job of comparing them.
The React client is built with support for a HAL-FORMS-based response. Thus, it sends application/prs.hal-forms+json
as an Accept
header on every request.
Now that you’re familiar with media types and identifiers, you’ll learn to enrich the hypermedia response in the upcoming sections.
Building the “Self” Link
The _links field is a reserved property. It contains one or more link objects. One of them is the self link, with which a client can get the full representation of the resource.
Adding a self link is straightforward. Go to the ArticleAssembler.kt. There, you’ll find buildSelfLink()
:
fun buildSelfLink(entity: ArticleEntity): Link {
return linkTo(
methodOn(ArticleController::class.java)
.getById(entity.id!!)
)
.withSelfRel()
}
Both linkTo()
and methodOn()
are static methods from org.springframework.hateoas.server.mvc.WebMvcLinkBuilder
. The getById()
is a method in ArticleController
, which is used in building a self link.
Finally, inside toModel()
, call this method by replacing the // TODO: Add Link here
with the snippet below:
val resourceLink = buildSelfLink(entity)
resource.add(resourceLink)
Again, build and run the project and execute the following command:
curl http://localhost:8080/articles/1 -H "Accept:application/hal+json"
It returns:
{
"id": 1,
"state": "DRAFT",
"title": "Getting Started with Cucumber",
"body": "[...]",
"updatedDate": "2022-05-22T23:43:33.990617",
"createdDate": "2022-05-22T23:43:33.990576",
"reviewType": "FOUR_LEVEL_WORKFLOW",
"_links": {
"self": {
"href": "http://localhost:8080/articles/1"
}
}
}
As you can see, it now includes the _links.self
field.
Before you continue enriching the response, you’ll first learn about affordances. To the next section!
Affordance
Hypermedias are resources and affordances. An article is a resource, and the actions you can perform on that resource are its affordances.
As mentioned in the “hypermedia affordances” blog, affordances are what the resource “offers” and how they offer it.
So updating an article by sending a PUT
request to /articles/1
can be one of its affordances. Moreover, they also need to be context-sensitive. If you aren’t allowed to edit or delete an article that’s already published, the affordances that allow for those two actions shouldn’t be available in the response.
Affordances also convey what “actions” are available, and clients can use that information to drive the application state forward. Hence, unlike resources, affordances are represented as verbs, not nouns.
To see these “actions” in action, open ArticleController.kt and check the handleAction()
:
@PostMapping("/{articleId}/{action}")
fun handleAction(
@PathVariable articleId: Long,
@PathVariable action: String
): RepresentationModel<ArticleResource> {
val event =
eventMapper.getArticleEvent(action) ?: throw IllegalArgumentException("$action is invalid")
service.handleEvent(articleId, event)
return articleAssembler.toModel(service.findById(articleId))
}
ArticleEventMapper
translates the “verbified” alias to the corresponding ArticleEvent
that the state machine consumes in the handleEvent()
. If that event is supported, it transitions into the next state while also updating the article’s state.