Gymterview
middle

Как реализовать пагинацию, фильтрацию и сортировку в REST API?

Пагинация, фильтрация и сортировка — обязательные механизмы для работы с большими коллекциями ресурсов, реализуемые через query-параметры HTTP-запросов.

Пагинация

Подход Пример Когда использовать
Offset-based GET /api/users?page=0&size=20 Наиболее распространённый, подходит для большинства случаев
Cursor-based GET /api/users?cursor=eyJpZCI6NDJ9&size=20 Для больших объёмов данных, real-time лент

Пример ответа с метаданными пагинации:

Пример
{
  "content": [...],
  "page": 0,
  "size": 20,
  "totalElements": 150,
  "totalPages": 8,
  "last": false,
  "_links": {
    "self": {"href": "/api/users?page=0&size=20"},
    "next": {"href": "/api/users?page=1&size=20"},
    "last": {"href": "/api/users?page=7&size=20"}
  }
}

Фильтрация

Пример
GET /api/users?status=active
GET /api/users?status=active&role=admin
GET /api/users?age_gte=18&age_lte=65
GET /api/users?search=Иван

Сортировка

Пример
GET /api/users?sort=name,asc
GET /api/users?sort=createdAt,desc&sort=name,asc
Реализация в Spring (Spring Data)
@RestController
@RequestMapping("/api/users")
public class UserController {

    private final UserRepository userRepository;

    // Пагинация и сортировка через Pageable
    @GetMapping
    public Page<User> getUsers(
            @RequestParam(required = false) String status,
            @RequestParam(required = false) String name,
            @PageableDefault(size = 20, sort = "id") Pageable pageable) {

        if (status != null) {
            return userRepository.findByStatus(status, pageable);
        }
        if (name != null) {
            return userRepository.findByNameContainingIgnoreCase(name, pageable);
        }
        return userRepository.findAll(pageable);
    }
}

Запрос:

GET /api/users?status=active&page=0&size=10&sort=name,asc

Для сложной фильтрации можно использовать Spring Data Specifications:

@GetMapping
public Page<User> getUsers(
        @RequestParam Map<String, String> filters,
        Pageable pageable) {

    Specification<User> spec = Specification.where(null);

    if (filters.containsKey("status")) {
        spec = spec.and((root, query, cb) ->
            cb.equal(root.get("status"), filters.get("status")));
    }
    if (filters.containsKey("name")) {
        spec = spec.and((root, query, cb) ->
            cb.like(cb.lower(root.get("name")),
                    "%" + filters.get("name").toLowerCase() + "%"));
    }

    return userRepository.findAll(spec, pageable);
}

На собеседовании: нужно знать оба подхода к пагинации (offset и cursor) и понимать, когда использовать какой. Частая ошибка — забыть про метаданные пагинации (totalElements, totalPages) в ответе или не упомянуть cursor-based подход.