In this article, I’ll show you how to add pagination to your REST API in a very simple manner. To implement the microservice, I’ll be using a combination of Spring Boot, Spring Data JPA and JPAstreamer.

Database setup

Before we start working on the actual microservice, I’ll show you how to set up the database I’ll be quering during this article. The database I’ll be using is called Sakila, a sample MySQL database containing data related to films and actors.

There are plenty of ways you can do the setup, but I’ll be using a Sakila Docker image. This means that if you want to follow along with my setup, you need to install Docker first. After you install Docker, run the following command from your terminal:

docker run -d --publish 3306:3306 restsql/mysql-sakila

This will start a local Sakila instance in the background. Now we dive into the code.

Adding Java dependencies

To make dependency management a tad bit easier, I’ll be using Maven as my build tool. This is the final pom.xml file for our project:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<parent>
	    <groupId>org.springframework.boot</groupId>
	    <artifactId>spring-boot-starter-parent</artifactId>
	    <version>2.3.4.RELEASE</version>
	    <relativePath/>
	</parent>

	<groupId>com.speedment.jpastreamer.example</groupId>
	<artifactId>pagination</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
	    <maven.compiler.source>11</maven.compiler.source>
	    <maven.compiler.target>11</maven.compiler.target>
	</properties>

	<dependencies>

            <dependency>
 		<groupId>com.speedment.jpastreamer</groupId>
		<artifactId>jpastreamer-core</artifactId>
		<version>0.1.8</version>
	    </dependency>
	    <dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	    </dependency>

	    <dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-jpa</artifactId>
	    </dependency>

            <dependency>
		<groupId>com.speedment.jpastreamer.integration.spring</groupId>
		<artifactId>spring-boot-jpastreamer-autoconfigure</artifactId>
		<version>0.1.8</version>
	    </dependency>

	    <dependency>
		<groupId>mysql</groupId>
		<artifactId>mysql-connector-java</artifactId>
		<version>8.0.21</version>
	    </dependency>

	    <dependency>
		<groupId>org.projectlombok</groupId>
		<artifactId>lombok</artifactId>
		<version>1.18.12</version>
		<scope>provided</scope>
	    </dependency>
       
	</dependencies>

	<build>
	    <plugins>
		<plugin>
          	    <groupId>org.springframework.boot</groupId>
		    <artifactId>spring-boot-maven-plugin</artifactId>
		</plugin>
	    </plugins>
        </build>
</project>

If you already have an existing project, you can simply add the missing dependencies. As a reference, we have the following dependencies:

The new kid on the block

The Spring family of frameworks and libraries is something I think needs no introduction. I consider them the veterans of the Java ecosystem. Even if you’ve been using Java only for a couple of months, there is a high chance that you’ve heard of Spring.

For the purposes of this article, I’m going to be using Spring Boot to create our very simple REST API. Additionally, I’m going to be using Spring Data JPA as the first part of our data access layer.

For the second part of our data access layer, I’m going to be using something new and exciting - JPAstreamer. If you’ve never used JPAstreamer, fear not as it’s exceptionally easy to use. What JPAstreamer allows us to do is construct JPA queries in a very understandable way using Java Streams. Since JPAstreamer is an extension for any JPA provider, it’s database agnostic, meaning it works with any database your JPA provider supports.

Now that we’re familiar with the libraries we’re going to be using, we can start writing our microservice.

Configuring our Spring Application

Before we start implementing our REST API, let’s configure our Spring app so it can access the Sakila database. In your resources folder create a file called application.properties and add the following lines to it:

spring.application.name=jpastreamer-pagination
spring.datasource.username=root
spring.datasource.password=sakila
spring.datasource.url=jdbc:mysql://localhost:3306/sakila
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

Creating our JPA Entities

I’m going to keep this microservice fairly simple, so I’ll be adding only a small amount of entities that we can work with. In the Sakila database there is a film table, so lets create an entity for that:

@Entity
@Table(name = "film")
@Data
public class Film {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "film_id", nullable = false, updatable = false, columnDefinition = "smallint(5)")
    private Integer filmId;

    @Column(name = "title", nullable = false, columnDefinition = "varchar(255)")
    private String title;

    @Column(name = "description", nullable = false, columnDefinition = "text")
    private String description;

    @ManyToOne
    @JoinColumn(name="language_id", nullable = false)
    private Language language;

    @ManyToMany(cascade = { CascadeType.ALL })
    @JoinTable(
            name = "film_actor",
            joinColumns = { @JoinColumn(name = "film_id") },
            inverseJoinColumns = { @JoinColumn(name = "actor_id") }
    )
    private List<Actor> actors = new ArrayList<>();

    @Column(name = "rental_duration", columnDefinition = "smallint(5)")
    private Integer rentalDuration;

    @Column(name = "rental_rate", columnDefinition = "decimal(4,2)")
    private Float rentalRate;

    @Column(name = "length", columnDefinition = "smallint(5)")
    private Integer length;

    @Column(name = "replacement_cost", columnDefinition = "decimal(5,2)")
    private Float replacementCost;

    @Column(name = "rating", columnDefinition = "enum('G','PG','PG-13','R','NC-17')")
    private String rating;

    @Column(name = "special_features", columnDefinition = "set('Trailers','Commentaries','Deleted Scenes','Behind the Scenes')")
    private String specialFeatures;

    @Column(name = "last_update", nullable = false, columnDefinition = "timestamp")
    private LocalDateTime lastUpdate;
}

The Film entity has references to Actor and Language, so let’s add those as well:

@Entity
@Table(name = "actor")
@Data
public class Actor {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "actor_id", nullable = false, updatable = false, columnDefinition = "smallint(5)")
    private Integer actorId;

    @Column(name = "first_name", nullable = false, columnDefinition = "varchar(45)")
    private String firstName;

    @Column(name = "last_name", nullable = false, columnDefinition = "varchar(45)")
    private String lastName;

    @ManyToMany(mappedBy = "actors")
    private List<Film> films = new ArrayList<>();
}
@Entity
@Table(name = "language")
@Data
public class Language {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "language_id", nullable = false, updatable = false, columnDefinition = "tinyint(3)")
    private Integer languageId;

    @Column(name = "name", nullable = false, columnDefinition = "char(20)")
    private String name;

    @OneToMany(mappedBy = "language")
    private Set<Film> films;
}

Creating our view models

A lot of the time, we only want to show our users a subset of data. In our case, most users are not interested in the replacement cost of the film or when it was last updated, but rather in the basic information such as the title of the film and actors that star in it. Because of this, we will create a view model for our Film entity that will only present specific bits of our Film:

@Getter
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(Include.NON_EMPTY)
public final class FilmViewModel {

    private final String title;
    private final String description;
    private final String language;
    private final List<String> actors;

    @JsonCreator
    public FilmViewModel(
            @JsonProperty("title") final String title,
            @JsonProperty("description") final String description,
            @JsonProperty("language") final String language,
            @JsonProperty("actors") final List<String> actors
    ) {
        this.title = requireNonNull(title);
        this.description = requireNonNull(description);
        this.language = requireNonNull(language);
        this.actors = requireNonNull(actors);
    }

    public static FilmViewModel from(final Film film) {
        requireNonNull(film);

        return new FilmViewModel(
            film.getTitle(),
            film.getDescription(),
            film.getLanguage().getName(),
            film.getActors().stream().map(actor -> actor.getFirstName() + " " + actor.getLastName()).collect(toList())
        );
    }
}

Creating our REST API

Now that we have our foundation, we can finally create our REST API. Let's start by creating the controller class:

@RestController
public class FilmController {

}

Next, let’s inject a JPAstreamer instance so we can use it:

private final JPAStreamer jpaStreamer;

@Autowired
public FilmController(JPAStreamer jpaStreamer) {
    this.jpaStreamer = jpaStreamer;
}

Finally, let’s create a GET mapping that will list our films:

@ResponseStatus(code = HttpStatus.OK)
@GetMapping(value = "/films", produces = MediaType.APPLICATION_JSON_VALUE)
public Stream<FilmViewModel> list(
    @RequestParam(required = false, defaultValue = "0") int page,
    @RequestParam(required = false, defaultValue = "10") int pageSize
) {
    return jpaStreamer.stream(Film.class)
        .skip(page * pageSize)
        .limit(pageSize)
        .map(FilmViewModel::from);
}

The final class should look like this:

@RestController
public class FilmController {

    private final JPAStreamer jpaStreamer;

    @Autowired
    public FilmController(JPAStreamer jpaStreamer) {
        this.jpaStreamer = jpaStreamer;
    }

    @ResponseStatus(code = HttpStatus.OK)
    @GetMapping(value = "/films", produces = MediaType.APPLICATION_JSON_VALUE)
    public List<FilmViewModel> list(
        @RequestParam(required = false, defaultValue = "0") int page,
        @RequestParam(required = false, defaultValue = "10") int pageSize
    ) {
        return jpaStreamer.stream(Film.class)
            .skip(page * pageSize)
            .limit(pageSize)
            .map(FilmViewModel::from);
    }
}

What this controller does is map our list method to the /films route, meaning any time a user executes a GET request on /films, our list method will be called. The list method also takes in 2 optional arguments, page and pageSize, which we can use to specify our pagination rules.

After being called, the list method executes a query using JPAstreamer’s Stream API.

Running our REST microservice

Before we can run our microservice, we need to add one key component - our main class. This can be called whatever you want, I’ve decided to call mine JpaStreamerPaginationApplication:

@SpringBootApplication
public class JpaStreamerPaginationApplication {

    public static void main(String[] args) {
	SpringApplication.run(JpaStreamerPaginationApplication.class, args);
    }
}

We can now run our microservice by executing the following command from the console:

mvn spring-boot:run

This will start our microservice on port 8080. Let’s try out a couple of requests to see if everything is working. I’ll start out by executing the simplest request we can - /films without any additional parameters:

GET /films

[
    {
        "title":"ACADEMY DINOSAUR",
        "description":"A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies",
        "language":"English",
        "actors":[
            "PENELOPE GUINESS",
            "CHRISTIAN GABLE",
            "LUCILLE TRACY",
            "SANDRA PECK",
            "JOHNNY CAGE",
            "MENA TEMPLE",
            "WARREN NOLTE",
            "OPRAH KILMER",
            "ROCK DUKAKIS",
            "MARY KEITEL"
        ]
    },
    {
        "title":"ACE GOLDFINGER",
        "description":"A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China",
        "language":"English",
        "actors":[
            "BOB FAWCETT",
            "MINNIE ZELLWEGER",
            "SEAN GUINESS",
            "CHRIS DEPP"
        ]
    },
    {
        "title":"ADAPTATION HOLES",
        "description":"A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory",
        "language":"English",
        "actors":[
            "NICK WAHLBERG",
            "BOB FAWCETT",
            "CAMERON STREEP",
            "RAY JOHANSSON",
            "JULIANNE DENCH"
        ]
    },
    {
        "title":"AFFAIR PREJUDICE",
        "description":"A Fanciful Documentary of a Frisbee And a Lumberjack who must Chase a Monkey in A Shark Tank",
        "language":"English",
        "actors":[
            "JODIE DEGENERES",
            "SCARLETT DAMON",
            "KENNETH PESCI",
            "FAY WINSLET",
            "OPRAH KILMER"
        ]
    },
    {
        "title":"AFRICAN EGG",
        "description":"A Fast-Paced Documentary of a Pastry Chef And a Dentist who must Pursue a Forensic Psychologist in The Gulf of Mexico",
        "language":"English",
        "actors":[
            "GARY PHOENIX",
            "DUSTIN TAUTOU",
            "MATTHEW LEIGH",
            "MATTHEW CARREY",
            "THORA TEMPLE"
        ]
    },
    {
        "title":"AGENT TRUMAN",
        "description":"A Intrepid Panorama of a Robot And a Boy who must Escape a Sumo Wrestler in Ancient China",
        "language":"English",
        "actors":[
            "KIRSTEN PALTROW",
            "SANDRA KILMER",
            "JAYNE NEESON",
            "WARREN NOLTE",
            "MORGAN WILLIAMS",
            "KENNETH HOFFMAN",
            "REESE WEST"
        ]
    },
    {
        "title":"AIRPLANE SIERRA",
        "description":"A Touching Saga of a Hunter And a Butler who must Discover a Butler in A Jet Boat",
        "language":"English",
        "actors":[
            "JIM MOSTEL",
            "RICHARD PENN",
            "OPRAH KILMER",
            "MENA HOPPER",
            "MICHAEL BOLGER"
        ]
    },
    {
        "title":"AIRPORT POLLOCK",
        "description":"A Epic Tale of a Moose And a Girl who must Confront a Monkey in Ancient India",
        "language":"English",
        "actors":[
            "FAY KILMER",
            "GENE WILLIS",
            "SUSAN DAVIS",
            "LUCILLE DEE"
        ]
    },
    {
        "title":"ALABAMA DEVIL",
        "description":"A Thoughtful Panorama of a Database Administrator And a Mad Scientist who must Outgun a Mad Scientist in A Jet Boat",
        "language":"English",
        "actors":[
            "CHRISTIAN GABLE",
            "ELVIS MARX",
            "RIP CRAWFORD",
            "MENA TEMPLE",
            "RIP WINSLET",
            "WARREN NOLTE",
            "GRETA KEITEL",
            "WILLIAM HACKMAN",
            "MERYL ALLEN"
        ]
    },
    {
        "title":"ALADDIN CALENDAR",
        "description":"A Action-Packed Tale of a Man And a Lumberjack who must Reach a Feminist in Ancient China",
        "language":"English",
        "actors":[
            "ALEC WAYNE",
            "JUDY DEAN",
            "VAL BOLGER",
            "RAY JOHANSSON",
            "RENEE TRACY",
            "JADA RYDER",
            "GRETA MALDEN",
            "ROCK DUKAKIS"
        ]
    }
]

This should give us the first 10 films. Now, let’s try playing around with the pagination options we’ve implemented. We’ll have our page contain only 5 films and we’ll get the 10th page:

GET /films?page=9&pageSize=5
[
    {
        "title":"AUTUMN CROW",
        "description":"A Beautiful Tale of a Dentist And a Mad Cow who must Battle a Moose in The Sahara Desert",
        "language":"English",
        "actors":[
            "DUSTIN TAUTOU",
            "ANGELA HUDSON",
            "JAMES PITT"
        ]
    },
    {
        "title":"BABY HALL",
        "description":"A Boring Character Study of a A Shark And a Girl who must Outrace a Feminist in An Abandoned Mine Shaft",
        "language":"English",
        "actors":[
            "NICK WAHLBERG",
            "MATTHEW JOHANSSON",
            "CHARLIZE DENCH",
            "DARYL WAHLBERG",
            "KEVIN GARLAND",
            "RIVER DEAN",
            "MINNIE KILMER",
            "VIVIEN BASINGER"
        ]
    },
    {
        "title":"BACKLASH UNDEFEATED",
        "description":"A Stunning Character Study of a Mad Scientist And a Mad Cow who must Kill a Car in A Monastery",
        "language":"English",
        "actors":[
            "CHRISTIAN AKROYD",
            "SPENCER PECK",
            "CHRISTOPHER BERRY",
            "SYLVESTER DERN",
            "DAN STREEP",
            "KEVIN GARLAND",
            "JANE JACKMAN"
        ]
    },
    {
        "title":"BADMAN DAWN",
        "description":"A Emotional Panorama of a Pioneer And a Composer who must Escape a Mad Scientist in A Jet Boat",
        "language":"English",
        "actors":[
            "BEN WILLIS",
            "HARRISON BALE",
            "CUBA ALLEN",
            "WARREN JACKMAN",
            "GRETA KEITEL",
            "OLYMPIA PFEIFFER",
            "ALAN DREYFUSS",
            "THORA TEMPLE"
        ]
    },
    {
        "title":"BAKED CLEOPATRA",
        "description":"A Stunning Drama of a Forensic Psychologist And a Husband who must Overcome a Waitress in A Monastery",
        "language":"English",
        "actors":[
            "MICHELLE MCCONAUGHEY"
        ]
    }
]

Summary

A lot of services we use nowadays have some form of pagination implemented into their APIs. In this article I showed you how to add pagination to your API in a very convenient way using JPAstreamer.


GitHub: github.com/speedment/jpa-streamer
Homepage: jpastreamer.org
Documentation: speedment.github.io/jpa-streamer
Gitter Support Chat: gitter.im/jpa-streamer