Skip to content
Spring Boot sb web 3 min read

Pagination & Sorting

Returning thousands of rows in one response is slow and wasteful. Spring lets a controller accept a Pageable parameter and return a Page<T>, automatically reading page, size, and sort from the query string and producing a response that carries the data plus pagination metadata. This page focuses on the web layer; for the persistence side see Pagination with JPA.

Pageable in controllers

Add a Pageable parameter and Spring resolves it from the request. Pass it straight to a Spring Data repository, which returns a Page<T>.

import org.springframework.data.domain.*;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductRepository repo;

    public ProductController(ProductRepository repo) {
        this.repo = repo;
    }

    @GetMapping
    public Page<Product> list(Pageable pageable) {
        return repo.findAll(pageable);
    }
}

Request:

curl "http://localhost:8080/api/products?page=0&size=2&sort=price,desc"

The query params map onto Pageable:

ParamMeaningExample
pageZero-based page indexpage=2
sizeItems per pagesize=20
sort`property,(ascdesc)` (repeatable)

The Page JSON response

A Page<T> serializes to the content plus rich metadata.

Output:

{
  "content": [
    { "id": 8, "name": "Monitor", "price": 299.0 },
    { "id": 3, "name": "Keyboard", "price": 89.99 }
  ],
  "pageable": { "pageNumber": 0, "pageSize": 2, "offset": 0 },
  "totalElements": 57,
  "totalPages": 29,
  "number": 0,
  "size": 2,
  "numberOfElements": 2,
  "first": true,
  "last": false,
  "sort": { "sorted": true, "unsorted": false }
}

Note: Spring Boot 3.3+ defaults to a stable, documented page serialization. To opt into it explicitly set spring.data.web.pageable.serialization-mode=via_dto, which avoids serializing the internal PageImpl structure and is the recommended forward-compatible choice.

Default page size and limits

Configure defaults and a hard maximum in application.properties so a client cannot request a million rows.

spring.data.web.pageable.default-page-size=20
spring.data.web.pageable.max-page-size=100
spring.data.web.pageable.one-indexed-parameters=false

Override per endpoint with @PageableDefault:

@GetMapping
public Page<Product> list(
        @PageableDefault(size = 25, sort = "name", direction = Sort.Direction.ASC)
        Pageable pageable) {
    return repo.findAll(pageable);
}

Returning DTOs, not entities

Map the page content to DTOs so you do not leak entities onto the wire. Page.map preserves all the metadata.

@GetMapping
public Page<ProductResponse> list(Pageable pageable) {
    return repo.findAll(pageable)
            .map(p -> new ProductResponse(p.getId(), p.getName(), p.getPrice()));
}

Custom metadata shape

If you do not want Spring’s default envelope, wrap results in your own record. This gives you full control over the JSON contract.

public record PagedResponse<T>(
        List<T> items,
        int page,
        int size,
        long totalElements,
        int totalPages,
        boolean last) {

    public static <T> PagedResponse<T> from(Page<T> page) {
        return new PagedResponse<>(
                page.getContent(), page.getNumber(), page.getSize(),
                page.getTotalElements(), page.getTotalPages(), page.isLast());
    }
}

@GetMapping
public PagedResponse<ProductResponse> list(Pageable pageable) {
    Page<ProductResponse> page = repo.findAll(pageable)
            .map(p -> new ProductResponse(p.getId(), p.getName(), p.getPrice()));
    return PagedResponse.from(page);
}

Output:

{
  "items": [ { "id": 8, "name": "Monitor", "price": 299.0 } ],
  "page": 0,
  "size": 20,
  "totalElements": 57,
  "totalPages": 3,
  "last": false
}

Sorting only (no paging)

When you want ordering without pages, accept a Sort parameter.

@GetMapping("/all")
public List<Product> all(Sort sort) {
    return repo.findAll(sort);   // ?sort=name,asc
}

Tip: Always sort on an indexed, unique-ish column (often the primary key as a tiebreaker). Without a deterministic sort, the same row can appear on two pages as data shifts between requests.

Pitfalls

  • page is zero-based by default; clients often assume 1-based — document it or set one-indexed-parameters=true.
  • Counting totalElements runs a separate COUNT query; for huge tables consider Slice<T> (no count) when you only need “is there a next page?”.
  • Returning a raw Page<Entity> can trigger lazy-loading serialization issues — map to DTOs.
Last updated June 13, 2026
Was this helpful?