Spring HATEOAS - Adding Pagination Links To RESTful API
Previously, we discussed how to add links to a REST API using Spring HATEOAS. This is a continuation of the previous article. In this section, we will look at how to add pagination links to API resources.
Spring data provides the org.springframework.data.web.PagedResourcesAssembler
class which is an implementation of the RepresentationModelAssembler, PagedModel>>
interface.
Considering a service that returns a pageable result of enzymes;
Page< EnzymeEntry> enzymes = enzymeService.getEnzymes(pageable);
The PagedResourcesAssembler< EnzymeEntry>
class is handy in converting our Page<EnzymeEntry>
to a PagedModel< EnzymeModel>
.
The overloaded toModel( … ) method creates a new PagedModel< EnzymeModel >
by converting the given Page< EnzymeEntry >
into a PageMetadata
instance and wrapping the contained elements into Page< EnzymeModel >
instances with pagination links.
Step. 1 – Implement the RepresentationModelAssembler
@Component
public class PaginationModelAssembler implements RepresentationModelAssembler<EnzymeEntry, EnzymeModel> {
private static final int LIMIT = 10;
private static final String ENZYME = "enzyme";
private static final String ASSOCIATED_PROTEINS = "associated Proteins";
@Override
public EnzymeModel toModel(EnzymeEntry enzyme) {
Class<EnzymeController> controllerClass = EnzymeController.class;
EnzymeModel model = buildEnzymeModel(enzyme);
model.add(linkTo(methodOn(controllerClass).findEnzymeByEcNumber(enzyme.getEc())).withRel(ENZYME));
model.add(linkTo(methodOn(controllerClass).findAssociatedProteinsByEcNumber(enzyme.getEc(), LIMIT)).withRel(ASSOCIATED_PROTEINS));
return model;
}
private EnzymeModel buildEnzymeModel(EnzymeEntry enzyme) {
return EnzymeModel.builder()
.ecNumber(enzyme.getEc())
.enzymeName(enzyme.getEnzymeName())
.enzymeFamily(enzyme.getEnzymeFamily())
.alternativeNames(enzyme.getAltNames())
.catalyticActivities(enzyme.getCatalyticActivities())
.cofactors(enzyme.getIntenzCofactors())
.associatedProteins(ProteinUtil.toCollectionModel(enzyme.getProteinGroupEntry()))
.build();
}
}
Step. 2 – Usage in Controller method
@RequestMapping(value = "/enzymes", produces = {MediaType.APPLICATION_JSON_VALUE, MediaTypes.HAL_JSON_VALUE})
@RestController
public class EnzymeController {
private final EnzymeService enzymeService;
private final PagedResourcesAssembler<EnzymeEntry> pagedResourcesAssembler;
private final PaginationModelAssembler paginationModelAssembler;
@Autowired
public EnzymeController(EnzymeService enzymeService, PagedResourcesAssembler<EnzymeEntry> pagedResourcesAssembler, PaginationModelAssembler paginationModelAssembler) {
this.enzymeService = enzymeService;
this.pagedResourcesAssembler = pagedResourcesAssembler;
this.paginationModelAssembler = paginationModelAssembler;
}
@GetMapping(value = "/")
public PagedModel<EnzymeModel> enzymes(@Parameter(description = "page number") @RequestParam(value = "page", defaultValue = "0", name = "page") int page, @Parameter(description = " result limit") @RequestParam(value = "size", defaultValue = "10") int size) {
Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "ecNumber");
Page<EnzymeEntry> enzymes = enzymeService.getEnzymeEntries(pageable);
return pagedResourcesAssembler.toModel(enzymes, paginationModelAssembler);
}
}
Step. 3 – HAL Output with pagination links
curl -X GET "http://localhost:8080/computingfacts/rest/enzymes/?page=1&size=2" -H "accept: application/hal+json"
{
"_embedded": {
"enzymes": [
{
"enzymeName": "Hydrolases",
"ecNumber": "3.-.-.-",
"enzymeFamily": "Hydrolases",
"alternativeNames": [],
"catalyticActivities": [],
"cofactors": [],
"associatedProteins": {
"_embedded": {
"proteins": [
{
"accession": "P96655",
"proteinName": "UPF0173 protein YddR",
"organismName": "Bacillus subtilis (strain 168)"
},
{
"accession": "O67893",
"proteinName": "Uncharacterized protein aq_2135",
"organismName": "Aquifex aeolicus (strain VF5)"
}
]
}
},
"_links": {
"enzyme": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/3.-.-.-"
},
"associated Proteins": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/3.-.-.-/proteins?limit=10"
}
}
},
{
"enzymeName": "Glycine N-methyltransferase",
"ecNumber": "2.1.1.20",
"enzymeFamily": "Transferases",
"alternativeNames": [],
"catalyticActivities": [],
"cofactors": [],
"associatedProteins": {
"_embedded": {
"proteins": [
{
"accession": "Q14749",
"proteinName": "Glycine N-methyltransferase",
"organismName": "Human"
},
{
"accession": "A0A0W8FGQ6",
"proteinName": "Glycine n-methyltransferase",
"organismName": "hydrocarbon metagenome"
}
]
}
},
"_links": {
"enzyme": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/2.1.1.20"
},
"associated Proteins": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/2.1.1.20/proteins?limit=10"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=2&sort=ecNumber,desc"
},
"prev": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/?page=0&size=2&sort=ecNumber,desc"
},
"self": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/?page=1&size=2&sort=ecNumber,desc"
},
"next": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/?page=2&size=2&sort=ecNumber,desc"
},
"last": {
"href": "http://localhost:8080/computingfacts/rest/enzymes/?page=3407&size=2&sort=ecNumber,desc"
}
},
"page": {
"size": 2,
"totalElements": 6815,
"totalPages": 3408,
"number": 1
}
}
In addition to the Page
information, the enzyme result is equipped with pagination links showing prev
, next
, last
etc.
The example code is available on GitHub. Please, drop me an email or leave a comment here. 😊
References
Spring HATEOAS - https://spring.io/projects/spring-hateoas
Spring Data doc - https://docs.spring.io/spring-data/data-commons/docs/current/api/org/springframework/data/web/PagedResourcesAssembler.html