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:
| Param | Meaning | Example |
|---|---|---|
page | Zero-based page index | page=2 |
size | Items per page | size=20 |
sort | `property,(asc | desc)` (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 internalPageImplstructure 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
pageis zero-based by default; clients often assume 1-based — document it or setone-indexed-parameters=true.- Counting
totalElementsruns a separateCOUNTquery; for huge tables considerSlice<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.