Manually Creating Wrappers
- Creating a
HalResourceWrapper
without an Embedded Resource - Creating a
HalResourceWrapper
with an Embedded Resource - Creating a
HalListWrapper
with Pagination
Reminder: Every example shown can be viewed and debugged in the hateoflux-demos repository. Clone or fork it and test as you explore options available! Either run the application and curl against the micro service or check the examples directly in the given unit tests.
Creating a HalResourceWrapper
without an Embedded Resource
To create a HalResourceWrapper
for a simple order without shipment details, the wrapper is implemented as shown below. This implementation typically resides within a controller method (or better yet, in an assembler):
@GetMapping("/order-no-embedded/{orderId}")
public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int orderId) { // 1
Mono<OrderDTO> orderMono = orderService.getOrder(orderId); // 2
return orderMono.map(order -> HalResourceWrapper.wrap(order) // 3
.withLinks( // 4
Link.of("orders/{orderId}/shipment") // 5
.expand(orderId) // 6
.withRel("shipment"), // 7
Link.linkAsSelfOf("orders/" + orderId) // 8
));
}
The numbered comments in the code correspond to the following explanations:
- Endpoint Mapping: The
@GetMapping("/{id}")
annotation maps HTTP GET requests to thegetOrder()
method. Note the generic types forHalResourceWrapper
. The first is the main resource, which is anOrderDTO
. Since we don’t have another embedded, i.e., secondary resource, the second generic is set toVoid
. - Fetching the Order:
orderService.getOrder()
is an arbitrary service call that could be reading a database or calling another service. - Wrapping the Order: The
Mono
is accessed, and the order is now wrapped.HalResourceWrapper.wrap()
creates already aHalResourceWrapper
. However, as it is now, no links or embedded resources are available. - Adding Links: This method adds an arbitrary number of links. Technically, there is no limit to how many links a resource can have. However, each
Link
needs a relation that is also present exactly once. So, even if multipleLinks
are added, each must be of a differentLinkRelation
. - Creating a New Link: With
Link.of()
a newLink
can be created manually. The provided string is interpreted as being thehref
of theLink
. In this case, it is a templated URI withid
as a variable. - Expanding the Template:
expand(id)
replaces the{id}
placeholder in the shipment link with the actual order ID, finalizing the URL. - Defining the Relation:
withRel()
assigns a relation name to the link, indicating its purpose in the context of the order resource. - Self Link:
withLinks()
in line 4 accepts an array of links (varargs).Link.linkAsSelfOf("orders/" + id)
generates aLink
with the relationSELF
, i.e., a self-referential link. This method is unique of its kind as it sets the relation and anhref
simultaneously.
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_links": {
"shipment": {
"href": "orders/1234/shipment"
},
"self": {
"href": "orders/1234"
}
}
}
The fields id
, total
, and status
are part of the OrderDTO
and were fetched by the OrderService
. The links were built in the code example above. Note that each relation is the key to the link’s attributes. In this case, we have two links with the relations “shipment” and “self”, while both provide only an href
.
Creating a HalResourceWrapper
with an Embedded Resource
The code might seem a bit lengthy; however, if you choose to use assemblers, they will handle most of it automatically for you (e.g. see here)!
Now we want to create a HalResourceWrapper
that doesn’t just reference a shipment via a link, but also includes the whole object instead:
@GetMapping("/order-with-embedded/{orderId}")
public Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> getOrderWithShipment(@PathVariable int orderId) { // 1
Mono<OrderDTO> orderMono = orderService.getOrder(orderId); // 2
Mono<ShipmentDTO> shipmentMono = shipmentService.getShipmentByOrderId(orderId); // 3
return orderMono.zipWith(shipmentMono, (order, shipment) ->
HalResourceWrapper.wrap(order) // 4
.withLinks( // 5
Link.linkAsSelfOf("orders/" + orderId)) // 6
.withEmbeddedResource( // 7
HalEmbeddedWrapper.wrap(shipment) // 8
.withLinks( // 9
linkTo(ShipmentController.class, c -> c.getShipment(shipment.getId())) // 10
.withRel(IanaRelation.SELF) // 11
.withHreflang("en-US") // 12
)
)
);
}
The numbered comments in the code correspond to the following explanations:
- Endpoint Definition: The
@GetMapping("/{id}")
annotation maps HTTP GET requests containing an order ID to thegetOrderWithShipment()
method. Note the generic types forHalResourceWrapper
. The first is the main resource, which is anOrderDTO
as before. However, since we now have an embedded resource, the second generic is set toShipmentDTO
. - Order Retrieval:
orderService.getOrder()
is an arbitrary service call that could be reading a database or calling another service. - Shipment Retrieval:
shipmentService.getShipmentByOrderId()
is an arbitrary service call that could be reading a database or calling another service. - Combining Order and Shipment Data:
orderMono.zipWith(shipmentMono, (order, shipment) -> ...)
merges the results of bothorderMono
andshipmentMono
. This combination allows the creation of aHalResourceWrapper
that encapsulates both the order and its associated shipment details. - Adding Links to the Order Resource: The
withLinks()
method attaches hypermedia links to the order resource. - Self Link for the Order:
Link.linkAsSelfOf("orders/" + orderId)
generates a self-referential link pointing to the current order resource. This time we didn’t use a template but simply concatenated thehref
. - Embedding the Shipment Resource: The
withEmbeddedResource()
adds an embedded resource to the main resource. - Wrapping Shipment Data for Embedding:
HalEmbeddedWrapper.wrap(shipment)
transforms theShipmentDTO
into aHalEmbeddedWrapper
. This wrapper prepares the shipment data for embedding within the order resource, ensuring it conforms to HAL standards. - Adding Links to the Shipment Resource: The
withLinks()
method is now part of theHalEmbeddedWrapper
and adds links to the embedded resource, not to the main one. - Creating a Self Link for the Shipment:
linkTo(ShipmentController.class, c -> c.getShipment(shipment.getId()))
is part of theSpringControllerLinkBuilder
. It constructs a link to the shipment resource by referencing thegetShipment()
method of theShipmentController
. This approach automatically builds thehref
based on the controller’s and the method’s endpoint by reading values from Spring’s annotations such as@RequestMapping
. It also automatically expands the URI templates and adds query parameters if any arguments are marked with@RequestParam
. - Defining the Relation Type for the Shipment Link:
withRel(IanaRelation.SELF)
assigns theself
relation type to the shipment link. This time we’re using theIanaRelation
enum, which defines a set of standardized relation types defined by IANA. - Specifying Additional Attributes of the Link:
withHreflang("en-US")
sets thehreflang
attribute of the shipment link to “en-US”. This attribute informs clients about the language of the linked resource, which can be useful for content negotiation and accessibility.
The serialized result of this HalResourceWrapper
is as follows:
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipment": { // notice the name is not "shipmentDTO"
"id": 127,
"carrier": "UPS",
"trackingNumber": "154-ASD-1238724",
"status": "Completed",
"_links": {
"self": {
"href": "/shipment/127",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "orders/1234"
}
}
}
The root fields are part of the main resource, OrderDTO
. The node _embedded
includes the embedded resource, ShipmentDTO
. Notice how the name of the object is not “shipmentDTO” but “shipment”. This is because the ShipmentDTO
class has an @Relation
annotation that defines how the class name should be written when it is serialized. Under _links
, we can also see the two attributes that we added to the self link. One is the expanded href
that the SpringControllerLinkBuilder
extracted from the controller class and method, and the other is the hreflang
we added.
Creating a HalListWrapper
with Pagination
The code might seem a bit lengthy; however, if you choose to use assemblers, they will handle most of it automatically for you (e.g. see here)!
To not deviate too much from the previous examples, lets consider the use case, where the user wants to list all his orders. It is quite possible to implement this as Flux
of HalResourceWrapper<OrderDTO>
. However, we decide to create a Mono
of HalListWrapper<OrderDTO>
:
import static de.kamillionlabs.hateoflux.utility.SortDirection.ASCENDING;
import static de.kamillionlabs.hateoflux.utility.SortDirection.DESCENDING;
@GetMapping("/orders-with-pagination")
public Mono<HalListWrapper<OrderDTO, Void>> getOrdersManualBuilt(@RequestParam Long userId, // 1
Pageable pageable, // 2
ServerWebExchange exchange) { // 3
Flux<OrderDTO> ordersFlux = orderService.getOrdersByUserId(userId, pageable); // 4
Mono<Long> totalElementsMono = orderService.countAllOrdersByUserId(userId); // 5
int pageSize = pageable.getPageSize();
long offset = pageable.getOffset();
List<SortCriteria> sortCriteria = pageable.getSort().get() // 6
.map(o -> SortCriteria.by(o.getProperty(), o.getDirection().isAscending() ? ASCENDING : DESCENDING)) // 7
.toList();
return ordersFlux.map(
order -> HalResourceWrapper.wrap(order) // 8
.withLinks(
Link.linkAsSelfOf("orders/" + order.getId())
.prependBaseUrl(exchange)))
.collectList() // 9
.zipWith(totalElementsMono,(ordersList, totalElements) -> { // 10
HalPageInfo pageInfo = HalPageInfo.assembleWithOffset(pageSize, totalElements, offset); // 11
return HalListWrapper.wrap(ordersList) // 12
.withLinks(Link.of("orders{?userId,someDifferentFilter}") // 13
.expand(userId) // 14
.prependBaseUrl(exchange) // 15
.deriveNavigationLinks(pageInfo, sortCriteria)) // 16
.withPageInfo(pageInfo); // 17
}
);
}
The numbered comments in the code correspond to the following explanations:
-
Endpoint Definition: The
@GetMapping
annotation maps HTTP GET requests to thegetOrders()
method. Note that the method returns aMono
of list instead of aFlux
of values. -
Accepting Pagination Parameters: In cases where Spring Data is used,
Pageable
can encapsulate pagination information such as page size, page number, and sorting criteria. These details can be used to create hateoflux-specific objects that will be utilized, among other things, for link building. -
Accessing Server Exchange Context:
ServerWebExchange
is automatically injected by Spring to provide access to the current HTTP request and response. It’s used later to prepend the base URL when constructing hypermedia links, ensuring they are absolute and correctly reference the server. -
Retrieving Paginated Orders:
orderService.getOrdersByUserId()
is a service call that retrieves a sorted list ofOrderDTO
objects for the specifieduserId
. In contrast to Spring Data, hateoflux transforms an arbitraryFlux
and does not rely on aPage
or page-like structure. -
Counting Total Number of Orders:
orderService.countAllOrdersByUserId()
returns aMono<Long>
representing the total number of orders for the givenuserId
. This total count is essential for constructing pagination metadata like total pages and total elements in the response. -
Extracting Sort Information: The
pageable.getSort().get()
method retrieves the sorting directives from thePageable
object. It returns a stream ofSort.Order
objects, each representing a sorting instruction on a particular field, such as sorting by date or price. -
Mapping to Internal Sort Criteria: The sorting stream is mapped to a list of
SortCriteria
objects. -
Wrapping Orders: The
ordersFlux.map()
operation transforms each individualOrderDTO
into aHalResourceWrapper<OrderDTO>
by wrapping it and adding hypermedia links. This is important because the end result, i.e.,HalListWrapper
, only accepts resources that are wrapped inHalResourceWrapper
and not raw resources on their own. -
Collecting Orders into a List: The
collectList()
method aggregates all theHalResourceWrapper<OrderDTO>
items into aList<HalResourceWrapper<OrderDTO>>
. -
Combining Orders with Total Elements: The
zipWith(totalElementsMono, (ordersList, totalElements) -> { ... })
subscribes to bothPublisher
s and makes them together available for further processing. -
Creating Pagination Metadata:
HalPageInfo.assembleWithOffset()
creates aHalPageInfo
object that contains pagination details like page size, total elements. -
Wrapping the Orders List in a HAL List Wrapper:
HalListWrapper.wrap(ordersList)
creates a wrapper around the list of order resources. Similar toHalResourceWrapper.wrap()
, the wrapper at this point only contains the resource and nothing else. -
Defining the Base Link for the Orders List:
Link.of("orders{?userId,someDifferentFilter}")
creates a templated link for theHalListWrapper
as a whole.userId
andsomeDifferentFilter
are query parameters that follow the structure defined in RFC6570, which hateoflux uses. -
Expanding Link Templates with Parameters:
expand(userId)
replaces the{userId}
placeholder in the link template with the actualuserId
value from the request parameters. Note thatsomeDifferentFilter
is missing and will therefore be simply ignored by the expansion, as query parameters are optional by nature. -
Appending Base URL to Links:
prependBaseUrl()
extracts the base URL from theServerWebExchange
and prefixes the defined href with it. This ensures that the link is absolute and correctly references the server’s address, which is crucial when the service is behind a proxy or load balancer. -
Adding Pagination Navigation Links:
deriveNavigationLinks(pageInfo, sortCriteria)
automatically generates standard pagination links (i.e.,first
,prev
,next
,last
as well asself
) based on the specified href in line 13, the current page information, and sorting criteria. Note that thehref
generally corresponds to the self link of the resource list. Navigation links are always relative to the href initially provided. This line also concludes the creation of links. -
Including Pagination Metadata in the Response:
withPageInfo()
attaches theHalPageInfo
object to theHalListWrapper
, providing clients with detailed pagination metadata. This includes information such as page size, total elements, total pages, and current page number.
The serialized result with example payload data of this HalListWrapper
is as follows:
{
"page": { //1
"size": 2,
"totalElements": 6,
"totalPages": 3,
"number": 0
},
"_embedded": { //2
"orderDTOs": [ //3
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_links": {
"self": {
"href": "http://myservice:8080/orders/1234"
}
}
},
{
"id": 1057,
"userId": 37,
"total": 72.48,
"status": "Delivered",
"_links": {
"self": {
"href": "http://myservice:8080/orders/1057"
}
}
}
]
},
"_links": { //4
"next": {
"href": "http://myservice:8080/orders?userId=37?page=1&size=2&sort=id,desc"
},
"self": {
"href": "http://myservice:8080/orders?userId=37?page=0&size=2&sort=id,desc"
},
"last": {
"href": "http://myservice:8080/orders?userId=37?page=2&size=2&sort=id,desc"
}
}
}
The JSON has a few interesting points worth highlighting. The numbered comments are explained as follows:
- Pagination Metadata: The
"page"
block contains pagination details provided bywithPageInfo()
. It includes fields such as:size
: Number of items per page (here, 2).totalElements
: Total number of items available (15).totalPages
: Total number of pages (8).number
: Current page index (starting from 0).
These fields mirror those in a HAL JSON response from a service using Spring HATEOAS, ensuring consistency in pagination representation.
-
Embedding Resources: In HAL documents, lists of resources are included under the
_embedded
key. While single resources add their fields at the top level, collections are nested within_embedded
. This structure allows clients to easily locate and parse embedded resources. -
Naming of Embedded Resources: The list of resources within
_embedded
must have a key that names the collection. In this example, the resources are under"orderDTOs"
, which is derived from the class nameOrderDTO
by converting it to lowercase and adding an “s” to pluralize. Since we did not use the@Relation
annotation to specify a custom relation name, the default naming convention applies. - Hypermedia Links and Navigation: The
_links
section includes hypermedia links generated by thederiveNavigationLinks()
method. It automatically created the"next"
,"self"
, and"last"
links. The"prev"
and"first"
links were omitted because they are not applicable:- The
number
field indicates we are on page0
, the first page, so there is no previous page. - The
"first"
link is redundant since"self"
already points to the first page. - Each link’s
href
includes query parameters such asuserId
,page
,size
, andsort
, reflecting the current request parameters and pagination state.
The inclusion of sorting parameters (e.g.,
sort=id,desc
) is optional but provides clients with complete context for the data they are viewing. - The