Overview
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
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.
Tutorial
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
Verify the prerequisites.
Before you begin, complete the following prerequisite tasks:
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.
Create a Quarkus project.
Navigate to the Quarkus Code Generator and configure your project with the following settings:
Select your preferred group and artifact ID.
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)
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>
Configure the MongoDB database.
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.
Create a data entity.
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; public class Fruit { private String id; 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; } public String toString() { return "Fruit{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}'; } public static Fruit of(String name) { Fruit fruit = new Fruit(); fruit.setName(name); return fruit; } }
Create a repository interface.
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; public interface FruitRepository extends BasicRepository<Fruit, String> { CursoredPage<Fruit> cursor(PageRequest pageRequest, Sort<Fruit> order); Page<Fruit> offSet(PageRequest pageRequest); long countBy(); }
The framework automatically implements this interface, allowing you to perform database operations without writing boilerplate code.
Create a database setup class.
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; 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( 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( ShutdownEvent ev) { LOGGER.info("The application is stopping..."); fruitRepository.deleteAll(fruitRepository.findAll().toList()); } }
Create REST API endpoints.
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; 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; } public Iterable<Fruit> offset( long page, int size) { var pageRequest = PageRequest.ofPage(page).size(size); return fruitRepository.offSet(pageRequest).content(); } public Iterable<Fruit> cursor( String after, String before, 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 thepagequery parameter./fruits/cursor: Supports cursor-based pagination by using theafterandbeforequery parameters.
Both endpoints also accept the size query parameter to specify
the number of items per page.
Test offset-based pagination.
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
Test cursor-based pagination.
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
Additional Resources
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.