Docs Menu
Docs Home
/ /
Third-Party Integrations

Tutorial: Paginate Data with Java and Quarkus

This tutorial shows you how to implement pagination techniques in a Quarkus application connected to MongoDB. You learn how to use the Jakarta Data repository to create REST API endpoints that support both offset-based and cursor-based pagination methods.

Pagination is a technique used to divide large datasets into smaller, more manageable chunks. This tutorial implements offset-based and cursor-based pagination methods. Offset-based pagination uses page numbers to retrieve specific subsets of data, while cursor-based pagination uses a reference point, or a cursor, to navigate through the dataset.

This tutorial shows how to perform the following actions:

  • Verify the prerequisites

  • Create a Quarkus project with the required dependencies

  • Configure the MongoDB connection

  • Define a data entity and repository

  • Implement REST API endpoints for pagination

  • Test the pagination endpoints

1

Before you begin, complete the following prerequisite tasks:

  • Install Java 21

  • Install Maven

  • Configure a MongoDB cluster, either on MongoDB Atlas or a local Docker instance

To start a local MongoDB instance with Docker, run the following command:

docker run --rm -d --name mongodb-instance -p 27017:27017 mongo

Alternatively, you can use MongoDB Atlas and deploy a free M0 cluster. To learn how to create an Atlas account and cluster, see the MongoDB Get Started guide.

2

Navigate to the Quarkus Code Generator and configure your project with the following settings:

  1. Select your preferred group and artifact ID.

  2. Add the following dependencies:

    • JNoSQL Document MongoDB (quarkus-jnosql-document-mongodb)

    • RESTEasy Reactive (quarkus-resteasy-reactive)

    • RESTEasy Reactive Jackson (quarkus-resteasy-reactive-jackson)

    • OpenAPI (quarkus-smallrye-openapi)

  3. Generate the project, download the ZIP file, and extract it.

Note

If you cannot find a dependency in the generator, add it manually to the pom.xml file.

After you complete the setup, verify that your pom.xml file includes the following dependencies:

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.jnosql</groupId>
<artifactId>quarkus-jnosql-document-mongodb</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
3

Open the application.properties file and add the following configuration properties to connect to your MongoDB instance:

quarkus.mongodb.connection-string = <your connection string>
jnosql.document.database = fruits

This configuration enables your application to connect to the MongoDB cluster at the specified connection string and use the fruits database.

Important

In production environments, enable access control and enforce authentication. For more information, see the Security Checklist.

You can override these properties by using environment variables, which allow you to specify different configurations for development, testing, and production without modifying your code.

4

Create a Fruit entity class in the src/main/java directory. The following code defines the entity with id and name fields:

import jakarta.nosql.Column;
import jakarta.nosql.Convert;
import jakarta.nosql.Entity;
import jakarta.nosql.Id;
import org.eclipse.jnosql.databases.mongodb.mapping.ObjectIdConverter;
@Entity
public class Fruit {
@Id
@Convert(ObjectIdConverter.class)
private String id;
@Column
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Fruit{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
public static Fruit of(String name) {
Fruit fruit = new Fruit();
fruit.setName(name);
return fruit;
}
}
5

Create a FruitRepository interface that extends the BasicRepository class. The following code defines methods for both offset and cursor-based pagination:

import jakarta.data.Sort;
import jakarta.data.page.CursoredPage;
import jakarta.data.page.Page;
import jakarta.data.page.PageRequest;
import jakarta.data.repository.BasicRepository;
import jakarta.data.repository.Find;
import jakarta.data.repository.OrderBy;
import jakarta.data.repository.Repository;
@Repository
public interface FruitRepository extends BasicRepository<Fruit, String> {
@Find
CursoredPage<Fruit> cursor(PageRequest pageRequest, Sort<Fruit> order);
@Find
@OrderBy("name")
Page<Fruit> offSet(PageRequest pageRequest);
long countBy();
}

The framework automatically implements this interface, allowing you to perform database operations without writing boilerplate code.

6

Create a SetupDatabase class in the src/main/java directory. The following code populates the database with sample data on startup and deletes the data on shutdown:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkus.runtime.ShutdownEvent;
import io.quarkus.runtime.StartupEvent;
import org.jboss.logging.Logger;
import java.util.List;
@ApplicationScoped
public class SetupDatabase {
private static final Logger LOGGER = Logger.getLogger(SetupDatabase.class.getName());
private final FruitRepository fruitRepository;
public SetupDatabase(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
void onStart(@Observes StartupEvent ev) {
LOGGER.info("The application is starting...");
long count = fruitRepository.countBy();
if (count > 0) {
LOGGER.info("Database already populated");
return;
}
List<Fruit> fruits = List.of(
Fruit.of("apple"),
Fruit.of("banana"),
Fruit.of("cherry"),
Fruit.of("date"),
Fruit.of("elderberry"),
Fruit.of("fig"),
Fruit.of("grape"),
Fruit.of("honeydew"),
Fruit.of("kiwi"),
Fruit.of("lemon")
);
fruitRepository.saveAll(fruits);
}
void onStop(@Observes ShutdownEvent ev) {
LOGGER.info("The application is stopping...");
fruitRepository.deleteAll(fruitRepository.findAll().toList());
}
}
7

Create a FruitResource class in the src/main/java directory. Then, paste the following code into the class file:

import jakarta.data.Sort;
import jakarta.data.page.PageRequest;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/fruits")
public class FruitResource {
private final FruitRepository fruitRepository;
private static final Sort<Fruit> ASC = Sort.asc("name");
private static final Sort<Fruit> DESC = Sort.desc("name");
public FruitResource(FruitRepository fruitRepository) {
this.fruitRepository = fruitRepository;
}
@Path("/offset")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Iterable<Fruit> offset(@QueryParam("page") @DefaultValue("1") long page,
@QueryParam("size") @DefaultValue("2") int size) {
var pageRequest = PageRequest.ofPage(page).size(size);
return fruitRepository.offSet(pageRequest).content();
}
@Path("/cursor")
@GET
@Produces(MediaType.APPLICATION_JSON)
public Iterable<Fruit> cursor(@QueryParam("after") @DefaultValue("") String after,
@QueryParam("before") @DefaultValue("") String before,
@QueryParam("size") @DefaultValue("2") int size) {
if (!after.isBlank()) {
var pageRequest = PageRequest.ofSize(size).afterCursor(PageRequest.Cursor.forKey(after));
return fruitRepository.cursor(pageRequest, ASC).content();
} else if (!before.isBlank()) {
var pageRequest = PageRequest.ofSize(size).beforeCursor(PageRequest.Cursor.forKey(before));
return fruitRepository.cursor(pageRequest, DESC).stream().toList();
}
var pageRequest = PageRequest.ofSize(size);
return fruitRepository.cursor(pageRequest, ASC).content();
}
}

This class defines the following endpoints:

  • /fruits/offset: Supports offset-based pagination by using the page query parameter.

  • /fruits/cursor: Supports cursor-based pagination by using the after and before query parameters.

Both endpoints also accept the size query parameter to specify the number of items per page.

8

Run the following command from your project directory to start your Quarkus application in development mode:

./mvnw compile quarkus:dev
9

In a separate terminal window, use the following curl commands to test the offset pagination endpoint. These commands request different pages of fruit data.

To fetch the first page, run the following command:

curl --location http://localhost:8080/fruits/offset?page=1

To fetch the second page, run the following command:

curl --location http://localhost:8080/fruits/offset?page=2

To fetch the fifth page, run the following command:

curl --location http://localhost:8080/fruits/offset?page=5
10

Use the following curl commands to test the cursor pagination endpoint. These commands use the after and before parameters to navigate through the dataset.

To fetch the initial set of fruits, run the following command:

curl --location http://localhost:8080/fruits/cursor

To fetch fruits with name field values that come after "banana", run the following command:

curl --location http://localhost:8080/fruits/cursor?after=banana

To fetch fruits with name field values that come before "date", run the following command:

curl --location http://localhost:8080/fruits/cursor?before=date

To learn more about pagination in MongoDB, see the Paginate Results guide in the MongoDB Atlas documentation.

To learn more about Quarkus, see the Quarkus documentation.

Back

Tutorial: Build a Quarkus Application with Panache and MongoDB

On this page