Spring Boot cookbook · Beginner

Paginate and sort safely with Spring Data

Expose a paginated list endpoint with client-chosen sorting, without leaking entities or allowing arbitrary sort fields.

Create a free account to save this page, take notes and track your progress. Sign in

Approach

Accept Pageable, cap the page size in config, map allowed sort keys to entity properties (rejecting others), query with a DTO projection, and return a stable page DTO instead of serializing PageImpl directly.

Solution

public record PageResponse<T>(List<T> items, int page, int size, long totalElements, int totalPages) {
    static <T> PageResponse<T> of(Page<T> p) {
        return new PageResponse<>(p.getContent(), p.getNumber(), p.getSize(), p.getTotalElements(), p.getTotalPages());
    }
}

@RestController
@RequestMapping("/api/v1/products")
class ProductListController {
    private static final Map<String, String> SORTABLE = Map.of("name", "name", "price", "priceCents", "newest", "createdAt");
    private final ProductRepository products;
    ProductListController(ProductRepository products) { this.products = products; }

    @GetMapping
    PageResponse<ProductSummary> list(@RequestParam(defaultValue = "0") @Min(0) int page,
                                      @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
                                      @RequestParam(defaultValue = "newest") String sort,
                                      @RequestParam(defaultValue = "desc") String dir) {
        String property = Optional.ofNullable(SORTABLE.get(sort))
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported sort: " + sort));
        Sort order = Sort.by("asc".equalsIgnoreCase(dir) ? Sort.Direction.ASC : Sort.Direction.DESC, property)
                .and(Sort.by("id"));                                         // tiebreaker for stable order
        return PageResponse.of(products.findAllSummaries(PageRequest.of(page, size, order)));
    }
}

interface ProductRepository extends JpaRepository<Product, Long> {
    @Query("select new com.example.shop.ProductSummary(p.id, p.name, p.priceCents) from Product p")
    Page<ProductSummary> findAllSummaries(Pageable pageable);
}

Pitfalls

  • Serializing Page/PageImpl directly (unstable JSON; Spring warns about it).
  • Allowing sort on any property (unindexed columns, nested paths).
  • Deep pages with OFFSET on huge tables — use keyset pagination (Window/ScrollPosition).

Variations

  • Spring Data's Window<T> + ScrollPosition.keyset() for cursor pagination.
  • @EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO) for a stable built-in format.

Further reading: Spring Data: Paging and sorting

Reference: https://docs.spring.io/spring-data/jpa/reference/repositories/query-methods-details.html#repositories.special-parameters

More Spring Boot recipes

Was this page helpful?