How to Create Custom Pagination Response in Spring Boot

When building REST APIs using Spring Boot, handling large datasets often requires pagination. While Spring Data JPA provides a straightforward way to paginate results using the Page interface, its default JSON response can be bulky and overloaded with metadata. Many developers prefer cleaner, custom pagination responses tailored to their specific needs.

This guide walks you through creating a custom pagination response in Spring Boot, covering everything from structuring a custom DTO to integrating HATEOAS-style navigation for even greater flexibility.

Default vs Custom Pagination

Default Pagination in Spring Boot

By default, when you use the Page interface to paginate results, Spring Boot returns the entire object, including unnecessary fields like pageable, sort, and size.

Example of a default Spring Data JPA pagination response:

{
  "content": [
    { "id": 1, "name": "John Doe" },
    { "id": 2, "name": "Jane Doe" }
  ],
  "pageable": {
    "sort": { "sorted": false, "unsorted": true, "empty": true },
    "pageSize": 2,
    "pageNumber": 0,
    ...
  },
  "totalPages": 5,
  "totalElements": 10,
  "last": false,
  "first": true,
  "number": 0,
  "size": 2,
  ...
}

This default response often includes redundant metadata that may confuse clients or require additional parsing. The large size of the response can also slow down API performance when dealing with minimal datasets or constrained environments.

Why Opt for a Custom Response?

A custom pagination response enables you to:

  • Keep the response lightweight, with only the necessary fields.
  • Simplify the structure for easier consumption by clients.
  • Include additional context, such as custom metadata or navigation links.

Create a DTO Like PagedResponse<T>

To organize your pagination response, create a reusable Data Transfer Object (DTO) to hold paginated data and metadata. Here’s an example of a PagedResponse<T> class:

public class PagedResponse<T> {
    private List<T> content;
    private int currentPage;
    private long totalItems;
    private int totalPages;
    private boolean hasNext;
    private boolean hasPrevious;

    // Constructor, Getters, Setters, and toString()
    public PagedResponse(List<T> content, int currentPage, long totalItems, int totalPages, boolean hasNext, boolean hasPrevious) {
        this.content = content;
        this.currentPage = currentPage;
        this.totalItems = totalItems;
        this.totalPages = totalPages;
        this.hasNext = hasNext;
        this.hasPrevious = hasPrevious;
    }
}

Key Fields:

  • content: The actual data.
  • currentPage: The current page number requested by the client.
  • totalItems: Total number of elements in the dataset.
  • totalPages: The number of pages available based on the dataset size and page size.
  • hasNext and hasPrevious: Flags indicating whether there are more pages available.

This DTO simplifies the pagination metadata and is reusable across API endpoints.

Extract Pagination Metadata

To build the PagedResponse, extract metadata such as total pages, current page, and navigation flags from the Page<T> object. The Page interface in Spring Data JPA provides methods for these directly.

Methods to Extract Metadata:

  • getTotalPages(): Retrieves the total number of pages.
  • getNumber(): Retrieves the current page (0-indexed).
  • getTotalElements(): Retrieves the total number of elements.
  • hasNext(): Indicates whether another page is available.
  • hasPrevious(): Indicates whether the current page is not the first one.

Here’s an example of a service method that constructs the custom response:

public <T> PagedResponse<T> createPagedResponse(Page<T> page) {
    return new PagedResponse<>(
            page.getContent(),
            page.getNumber(),
            page.getTotalElements(),
            page.getTotalPages(),
            page.hasNext(),
            page.hasPrevious()
    );
}

Transform Page<T> into PagedResponse<T>

Controller-Level Implementation

Update your controller method to use the custom DTO for paginated responses. Here’s how you can implement it:

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public PagedResponse<User> getUsers(
            @RequestParam int page,
            @RequestParam int size,
            @RequestParam(required = false) String sort,
            @RequestParam(required = false) String direction) {

        Sort sortOrder = direction != null && direction.equalsIgnoreCase("desc")
                ? Sort.by(sort).descending()
                : Sort.by(sort).ascending();
        Pageable pageable = PageRequest.of(page, size, sortOrder);

        Page<User> userPage = userRepository.findAll(pageable);
        return createPagedResponse(userPage);
    }
}

Returning the PagedResponse ensures that your API output is cleaner, with only the necessary details included.

Example JSON with Simplified Fields

Here’s an example of the custom pagination JSON response:

{
  "content": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "currentPage": 0,
  "totalItems": 10,
  "totalPages": 5,
  "hasNext": true,
  "hasPrevious": false
}

This format is concise and includes all essential fields, making it easier for clients to work with.

Bonus: Add Links for HATEOAS-Style Navigation

For APIs that follow the HATEOAS principle, you can include navigation links (like next, previous, first, last) in the response. This allows clients to easily access neighboring pages without constructing URLs manually.

Updated PagedResponse DTO

Add navigation links to the DTO:

public class PagedResponse<T> {
    private List<T> content;
    private int currentPage;
    private long totalItems;
    private int totalPages;
    private boolean hasNext;
    private boolean hasPrevious;
    private String nextPage;
    private String previousPage;

    // Constructor, Getters, Setters
}

Generating Links

Generate and set links dynamically based on page metadata:

private String generatePageLink(int page, int size) {
    return String.format("/users?page=%d&size=%d", page, size);
}

public <T> PagedResponse<T> createPagedResponseWithLinks(Page<T> page) {
    String nextPageLink = page.hasNext() ? generatePageLink(page.getNumber() + 1, page.getSize()) : null;
    String previousPageLink = page.hasPrevious() ? generatePageLink(page.getNumber() - 1, page.getSize()) : null;

    return new PagedResponse<>(
            page.getContent(),
            page.getNumber(),
            page.getTotalElements(),
            page.getTotalPages(),
            page.hasNext(),
            page.hasPrevious(),
            nextPageLink,
            previousPageLink
    );
}

Example Response with Links

{
  "content": [
    { "id": 1, "name": "Alice" },
    { "id": 2, "name": "Bob" }
  ],
  "currentPage": 0,
  "totalItems": 10,
  "totalPages": 5,
  "hasNext": true,
  "hasPrevious": false,
  "nextPage": "/users?page=1&size=2",
  "previousPage": null
}

Final Thoughts

Creating a custom pagination response in Spring Boot helps reduce response size, simplify client-side parsing, and improve API usability. By transforming the default Page<T> into a custom DTO like PagedResponse<T> and adding optional HATEOAS-style links, you can build more developer-friendly and efficient APIs.

Start implementing a custom pagination response today to enhance your Spring Boot APIs and provide a better experience for clients!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *