Custom Pagination Response in Spring Boot JPA with PostgreSQL
Custom pagination response in spring boot jpa with postgresql, Spring Data JPA pagination with custom query, Spring Boot pagination with custom query, Spring Boot pagination, sorting and filtering, Spring JPA pagination with native query example, Pageable Spring Boot, Pagination in Spring Boot REST API with JPA
When your application needs to handle large datasets, pagination becomes a crucial factor. With Spring Boot and JPA, paginated data can be effortlessly retrieved. However, the default Page<T>
JSON response provided by Spring Data contains many unnecessary details that often complicate integrations with frontend frameworks or APIs. Customizing the pagination response helps you deliver a more readable, concise, and user-friendly structure that aligns with client expectations.
This guide will walk you through why and how to customize pagination responses in Spring Boot with JPA and PostgreSQL. By implementing these steps, your API can provide high-value responses tailored for seamless frontend integrations.
Table of Contents
- Why Customize the Default Page<T> JSON?
- Define a Custom DTO for Pagination Metadata
- Map Page<T> to Custom Structure
- Remove Unnecessary Internal Details
- Send TotalElements, TotalPages, HasNext, etc.
- Generic PaginatedResponse<T> Class
- Refactor Controller for Consistent Responses
- Improve API Documentation with Swagger/OpenAPI
- Include Custom Headers (Optional)
- Final JSON Response Before/After Comparison
Why Customize the Default Page<T> JSON?
Spring Data JPA’s Page<T>
interface provides pagination for repository operations, outputting JSON containing the paginated content and metadata. However, by default, Page<T>
JSON responses include verbose and extraneous details such as internal pagination mechanisms (pageable
) or sorting configurations (sort
). While this data might be helpful for backend systems, it creates unnecessary complexity for API consumers.
Common Issues with Default JSON:
- Cluttered Output: The response includes excessive fields like
pageable
,sort
, and nested classes irrelevant to API users. - Confusion for Frontend Developers: Clients only care about relevant details such as page size, total items, or content.
- Lack of Consistency: Responses vary across endpoints, making it harder to maintain a standardized API style.
Simplifying the JSON makes your API cleaner, increases usability, and facilitates frontend integrations.
Example Scenarios:
- A dashboard application displaying paginated product data.
- A public API endpoint consumed by third-party developers.
Customizing the response eliminates unnecessary data while maintaining the complete context required by clients.
Define a Custom DTO for Pagination Metadata
To provide a clean and concise pagination structure, define a custom DTO (Data Transfer Object) that encapsulates only the relevant metadata and paginated content.
Custom PaginatedResponse<T> DTO:
public class PaginatedResponse<T> { private List<T> content; private int currentPage; private int totalPages; private long totalItems; private boolean hasNext; public PaginatedResponse(List<T> content, int currentPage, int totalPages, long totalItems, boolean hasNext) { this.content = content; this.currentPage = currentPage; this.totalPages = totalPages; this.totalItems = totalItems; this.hasNext = hasNext; } // Getters and setters }
Benefits:
- Simplicity: Focuses on what clients need (data + metadata).
- Flexibility: Easily extendable to include additional fields if needed.
Map Page<T> to Custom Structure
Spring Data’s Page<T>
object already provides metadata such as the current page, total pages, and total elements. You can transfer these details into the custom DTO to provide a cleaner response format.
Mapping Example:
public <T> PaginatedResponse<T> mapToPaginatedResponse(Page<T> page) { return new PaginatedResponse<>( page.getContent(), page.getNumber(), page.getTotalPages(), page.getTotalElements(), page.hasNext() ); }
This mapping function transforms the Page<T>
response into the cleaner PaginatedResponse<T>
format.
Remove Unnecessary Internal Details
Internal structures like pageable
and sorting metadata are often irrelevant to API consumers. Removing these fields simplifies responses and reduces payload size.
Default JSON Example:
{ "content": [ { "id": 1, "name": "Product A" }, { "id": 2, "name": "Product B" } ], "pageable": { ... }, "sort": { ... }, "totalPages": 3, "totalElements": 50 }
Simplified JSON Example:
{ "content": [ { "id": 1, "name": "Product A" }, { "id": 2, "name": "Product B" } ], "currentPage": 0, "totalPages": 3, "totalItems": 50, "hasNext": true }
A concise response is easier to read, parse, and integrate with frontend applications.
Send TotalElements, TotalPages, HasNext, etc.
Fields like totalItems
(number of items in the dataset) and hasNext
(boolean flag indicating another page’s availability) are essential for pagination controls in the frontend. Including these ensures that API responses are actionable and informative.
Generic PaginatedResponse<T> Class
To avoid hardcoding for specific entities, define PaginatedResponse<T>
as a generic class. This allows the DTO to handle paginated responses for any entity type (Product
, Order
, User
, etc.) without duplication.
Example Usage:
For Product
:
Page<Product> products = productRepository.findAll(pageable); PaginatedResponse<Product> response = mapToPaginatedResponse(products);
For Order
:
Page<Order> orders = orderRepository.findAll(pageable); PaginatedResponse<Order> response = mapToPaginatedResponse(orders);
Refactor Controller for Consistent Responses
Update your controllers to return PaginatedResponse<T>
instead of the default Page<T>
structure.
Controller Code:
@RestController @RequestMapping("/api/products") public class ProductController { private final ProductService productService; @GetMapping public PaginatedResponse<Product> getProducts(Pageable pageable) { Page<Product> pagedProducts = productService.getProducts(pageable); return mapToPaginatedResponse(pagedProducts); } }
This ensures response consistency across all endpoints.
Improve API Documentation with Swagger/OpenAPI
Documenting customized pagination responses is critical. Use Swagger/OpenAPI annotations to describe the response structure.
Example:
@Operation(summary = "Get paginated list of products") @ApiResponse( responseCode = "200", description = "A paginated response with product details", content = @Content(schema = @Schema(implementation = PaginatedResponse.class)) )
Swagger provides your API consumers with clear expectations regarding response format and metadata.
Include Custom Headers (Optional)
Adding custom headers for pagination metadata can further enhance flexibility by removing the need to embed pagination metadata in the JSON body.
Example Headers:
X-Total-Count
: Total results in the dataset.X-Total-Pages
: Total number of pages.
Final JSON Response Before/After Comparison
Before (Default):
{ "content": [...], "pageable": { ... }, "sort": { ... }, "totalPages": 3, "totalElements": 50, ... }
After (Customized):
{ "content": [...], "currentPage": 1, "totalPages": 3, "totalItems": 50, "hasNext": true }
FAQs
Q1. Why customize pagination responses in Spring Boot?
To simplify the response, making it easier for frontend developers and API consumers to use.
Q2. What is the difference between Page and PaginatedResponse<T>?
Page<T>
is the default structure provided by Spring Data, while PaginatedResponse<T>
is a customized DTO.
Q3. How does Swagger help with custom pagination responses?
Swagger improves API documentation by visually showing the structure, helping developers integrate seamlessly.
Customize your Spring Boot pagination responses today for cleaner, more maintainable, and user-friendly APIs!