5. Paginated List - Spring Boot vs Micronaut Framework - Building a Rest API

This guide compares how to create a GET endpoint that returns a paginated JSON Array in Micronaut and Spring Boot applications.

Authors: Sergio del Amo

Micronaut Version: 4.6.1

1. Sample Project

You can download a sample application with the code examples in this article.

2. Introduction

This guide compares how to write a POST endpoint backed by a persistence layer in a Micronaut Framework and Spring Boot applications.

The Spring Boot application uses Spring Data and H2. The Micronaut application uses Micronaut Data and H2. Micronaut Data is a database access toolkit that uses Ahead of Time (AoT) compilation to pre-compute queries for repository interfaces that are then executed by a thin, lightweight runtime layer.

This guide is the fourth tutorial of Building a Rest API - a series of tutorials comparing how to develop a REST API with Micronaut Framework and Spring Boot.

3. Pagination

When returning multiple records you need some control over paging the data. Micronaut Data includes the ability to specify pagination requirements with the Pageable type. The same concepts exists in Spring Data with org.springframework.data.domain.Pageable.

4. Repository

Micronaut Data and Spring Data enhance the repository pattern by providing a paginated interfaces from which you can extend.

Spring Data package is org.springframework.data.repository and Micronaut Data package is io.micronaut.data.repository.

Table 1. Comparison between Spring Boot and Micronaut Framework
Spring Data Micronaut Data

Created, Read, Update, Delegate Operations

CrudRepository

CrudRepository

Paginated Operations

PagingAndSortingRepository

PageableRepository

4.1. Spring Boot

The repository in the Spring Boot applications extends from both CrudRepository and PagingAndSortingRepository.

springboot/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;

interface SaasSubscriptionRepository extends CrudRepository<SaasSubscription, Long>, (1)
        PagingAndSortingRepository<SaasSubscription, Long> { (2)
}
1 By extending CrudRepository you enable automatic generation of CRUD (Create, Read, Update, Delete) operations.
2 Extend PagingAndSortingRepository to support paging and sorting.

4.2. Micronaut

The repository in the Micronaut application extends from both PageableRepository.

micronautframework/src/main/java/example/micronaut/SaasSubscriptionRepository.java
package example.micronaut;

import io.micronaut.data.jdbc.annotation.JdbcRepository;
import io.micronaut.data.model.query.builder.sql.Dialect;
import io.micronaut.data.repository.PageableRepository;

@JdbcRepository(dialect = Dialect.H2) (1)
interface SaasSubscriptionRepository extends PageableRepository<SaasSubscription, Long> { (2)
}
1 @JdbcRepository with a specific dialect.
2 PageableRepository extends CrudRepository, which provides automatic generation of CRUD (Create, Read, Update, Delete) operations and adds methods for pagination.

5. Controller

Both frameworks simplify the creation of paginated endpoints by allowing the binding of Pageable as a controller method parameter.

5.1. Spring Boot Controller

In the Spring Boot application, we build a PageRequest, a basic Java Bean implementation of Pageable, with the Pageable parameter and then pass it to the repository.

springboot/src/main/java/example/micronaut/SaasSubscriptionGetListController.java
package example.micronaut;

import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController (1)
@RequestMapping("/subscriptions") (2)
public class SaasSubscriptionGetListController {

    private static final Sort DEFAULT_SORT = Sort.by(Sort.Direction.ASC, "cents");
    private final SaasSubscriptionRepository repository;

    private SaasSubscriptionGetListController(SaasSubscriptionRepository repository) { (3)
        this.repository = repository;
    }

    @GetMapping (4)
    private ResponseEntity<List<SaasSubscription>> findAll(Pageable pageable) {
        PageRequest pageRequest = pageRequestOf(pageable);
        Page<SaasSubscription> page = repository.findAll(pageRequest);
        return ResponseEntity.ok(page.getContent());
    }

    private static PageRequest pageRequestOf(Pageable pageable) {
        return PageRequest.of(
                pageable.getPageNumber(),
                pageable.getPageSize(),
                pageable.getSortOr(DEFAULT_SORT)
        );
    }
}
1 Annotate a class @RestController to identify the class as a Component capable of handling HTTP requests.
2 @RequestMapping identifies the request paths that invoke this Controller.
3 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
4 The @GetMapping annotation maps the findAll method to an HTTP GET request on /subscriptions.

5.2. Micronaut Controller

In the Micronaut application, we pass a Pageable instance to the repository.

micronautframework/src/main/java/example/micronaut/SaasSubscriptionGetListController.java
package example.micronaut;

import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.data.model.Sort;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/subscriptions") (1)
class SaasSubscriptionGetListController {

    private static final Sort CENTS = Sort.of(Sort.Order.asc("cents"));
    private final SaasSubscriptionRepository repository;

    SaasSubscriptionGetListController(SaasSubscriptionRepository repository) { (2)
        this.repository = repository;
    }

    @Get (3)
    Iterable<SaasSubscription> findAll(Pageable pageable) { (4)
        Page<SaasSubscription> page = repository.findAll(pageable.getSort().isSorted()
                ? pageable
                : Pageable.from(pageable.getNumber(), pageable.getSize(), CENTS)
        );  (5)
        return page.getContent();
    }
}
1 The class is defined as a controller with the @Controller annotation mapped to the path /subscriptions.
2 Use constructor injection to inject a bean of type SaasSubscriptionRepository.
3 The @Get annotation maps the findAll method to an HTTP GET request on /subscriptions.
4 Pageable includes the ability to specify pagination requirements. You can bind it as a controller method parameter.
5 PageableRepository provides a findAll method which takes a Pageable parameter.

6. Tests

In this tutorial, we use AssertJ in the tests. Moreover, we use Jayway JsonPath - a Java DSL for reading JSON documents.

In this tutorial, we use Json-smart in the tests.

Json-smart is a performance focused, JSON processor lib.

6.1. See Data

The seed data used in the tests is the same for both applications. We modified it to contain multiple entries.

springboot/src/test/resources/data.sql
INSERT INTO saas_subscription(id, name, cents) VALUES (99, 'Advanced', 2900);
INSERT INTO saas_subscription(id, name, cents) VALUES (100, 'Essential', 1400);
INSERT INTO saas_subscription(id, name, cents) VALUES (101, 'Professional', 4900);

6.2. Spring Boot Test

The following tests verify the following scenarios:

  • Returning a list specifying the sorting

  • Returning a list with the default sorting

  • Returning a list specifying no pageable parameters

springboot/src/test/java/example/micronaut/SaasSubscriptionGetListControllerTest.java
package example.micronaut;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) (1)
class SaasSubscriptionGetListControllerTest {

    @Autowired (2)
    TestRestTemplate restTemplate; (3)

    @Test
    void shouldReturnASortedPageOfSaasSubscriptions() {
        ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions?page=0&size=1&sort=cents,desc", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        DocumentContext documentContext = JsonPath.parse(response.getBody());
        JSONArray read = documentContext.read("$[*]");
        assertThat(read.size()).isEqualTo(1);

        Integer cents = documentContext.read("$[0].cents");
        assertThat(cents).isEqualTo(4900);

        response = restTemplate.getForEntity("/subscriptions?page=0&size=1", String.class);
        documentContext = JsonPath.parse(response.getBody());
        cents = documentContext.read("$[0].cents");
        assertThat(cents).isEqualTo(1400);
    }

    @Test
    void shouldReturnAPageOfSaasSubscriptions() {
        ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions?page=0&size=1", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        DocumentContext documentContext = JsonPath.parse(response.getBody());
        JSONArray page = documentContext.read("$[*]");
        assertThat(page.size()).isEqualTo(1);
    }

    @Test
    void shouldReturnAllSaasSubscriptionsWhenListIsRequested() {
        ResponseEntity<String> response = restTemplate.getForEntity("/subscriptions", String.class);
        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);

        DocumentContext documentContext = JsonPath.parse(response.getBody());
        int saasSubscriptionCount = documentContext.read("$.length()");
        assertThat(saasSubscriptionCount).isEqualTo(3);

        JSONArray ids = documentContext.read("$..id");
        assertThat(ids).containsExactlyInAnyOrder(99, 100, 101);

        JSONArray cents = documentContext.read("$..cents");
        assertThat(cents).containsExactlyInAnyOrder(1400, 2900, 4900);
    }

}
1 The @SpringBootTest annotation tells Spring Boot to look for a main configuration class (one with @SpringBootApplication, for instance) and use that to start a Spring application context.
2 Inject a bean of type TestRestTemplate by using @Autowired on the field definition.
3 TestRestTemplate is a helper to ease to execution of HTTP requests against the locally running application.

6.3. Micronaut Test

The Micronaut Test is almost identical to the previous Spring Boot test. The main difference is that the Micronaut Test uses the built-in HttpClient instead of TestRestTemplate and HttpResponse instead of ResponseEntity.

micronautframework/src/test/java/example/micronaut/SaasSubscriptionGetListControllerTest.java
package example.micronaut;

import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import io.micronaut.http.HttpRequest;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.HttpStatus;
import io.micronaut.http.client.BlockingHttpClient;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.uri.UriBuilder;
import io.micronaut.test.annotation.Sql;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import net.minidev.json.JSONArray;
import org.junit.jupiter.api.Test;

import java.net.URI;

import static org.assertj.core.api.Assertions.assertThat;

@Sql(value = {"classpath:schema.sql", "classpath:data.sql"}) (1)
@MicronautTest (2)
class SaasSubscriptionGetListControllerTest {

    @Inject
    @Client("/")
    HttpClient httpClient; (3)

    @Test
    void shouldReturnASortedPageOfSaasSubscriptions() {
        BlockingHttpClient client = httpClient.toBlocking();
        URI uri = UriBuilder.of("/subscriptions")
                .queryParam("page", 0)
                .queryParam("size", 1)
                .queryParam("sort", "cents,desc")
                .build();
        HttpResponse<String> response = client.exchange(HttpRequest.GET(uri), String.class);
        assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());

        DocumentContext documentContext = JsonPath.parse(response.body());
        JSONArray page = documentContext.read("$[*]");
        assertThat(page).hasSize(1);

        Integer cents = documentContext.read("$[0].cents");
        assertThat(cents).isEqualTo(4900);
    }

    @Test
    void shouldReturnAPageOfSaasSubscriptions() {
        BlockingHttpClient client = httpClient.toBlocking();
        URI uri = UriBuilder.of("/subscriptions")
                .queryParam("page", 0)
                .queryParam("size", 1)
                .build();
        HttpResponse<String> response = client.exchange(HttpRequest.GET(uri), String.class);
        assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());

        DocumentContext documentContext = JsonPath.parse(response.body());
        JSONArray page = documentContext.read("$[*]");
        assertThat(page.size()).isEqualTo(1);
    }

    @Test
    void shouldReturnAllSaasSubscriptionsWhenListIsRequested() {
        BlockingHttpClient client = httpClient.toBlocking();
        HttpResponse<String> response = client.exchange("/subscriptions", String.class);
        assertThat(response.status().getCode()).isEqualTo(HttpStatus.OK.getCode());

        DocumentContext documentContext = JsonPath.parse(response.body());
        int saasSubscriptionCount = documentContext.read("$.length()");
        assertThat(saasSubscriptionCount).isEqualTo(3);

        JSONArray ids = documentContext.read("$..id");
        assertThat(ids).containsExactlyInAnyOrder(99, 100, 101);

        JSONArray cents = documentContext.read("$..cents");
        assertThat(cents).containsExactlyInAnyOrder(1400, 2900, 4900);
    }
}
1 @Sql allows you to load SQL before tests
2 Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info.
3 Inject the HttpClient bean and point it to the embedded server.

7. Conclusion

Adding pagination to the persistence layer is easy in both frameworks, and the API is almost identical. However, Micronaut Data’s compile-time/reflection-free approach results in better performance, smaller stack traces, and reduced memory consumption.

8. License

All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…​).