Build a Spring Boot REST API with Pagination, Filtering, and Sorting

Building an efficient and flexible REST API is key to managing dynamic data in modern applications. Pagination, filtering, and sorting are three critical features often required to ensure manageable payload sizes, enhance user experience, and allow precise control over data retrieval. This guide will walk you through building a Spring Boot REST API that includes these features, starting from project setup to advanced techniques like dynamic filtering.

Setting Up Spring Boot Project with Spring Data JPA

First, create a Spring Boot project using Spring Initializr or your preferred development environment. Add the following dependencies to your project:

  • Spring Web (for building REST APIs)
  • Spring Data JPA (for database access)
  • H2 Database (or any database of your choice)

Your pom.xml or build.gradle should include:

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    runtimeOnly 'com.h2database:h2'
}

Configure your application.properties to set up the datasource:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect

Run your application and confirm it initializes properly.


Entity + Repository + Service Layer

Create an Entity

Define an entity, e.g., User, which maps to a database table:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private int age;

    // Getters and Setters
}

Create a Repository

Extend the JpaRepository for CRUD operations and built-in support for pagination:

public interface UserRepository extends JpaRepository<User, Long> {
}

Create a Service Layer

Encapsulate business logic in a service class:

@Service
public class UserService {

    @Autowired
    private UserRepository userRepository;

    public Page<User> getUsers(Pageable pageable) {
        return userRepository.findAll(pageable);
    }
}

Using this layered architecture will make your code more maintainable and scalable.


Implement Filtering Using @RequestParam

Basic Filtering

Allow filtering based on fields like name or email by adding @RequestParam to your controller:

@RestController
@RequestMapping("/users")
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping
    public Page<User> getUsers(@RequestParam(required = false) String name, 
                               @RequestParam int page, 
                               @RequestParam int size) {
        Pageable pageable = PageRequest.of(page, size);
        if (name != null) {
            return userRepository.findByNameContaining(name, pageable);
        }
        return userService.getUsers(pageable);
    }
}

To make this work, add a custom query method in UserRepository:

Page<User> findByNameContaining(String name, Pageable pageable);

This allows filtering users by partial matches on the name field. For example, /users?name=John&page=0&size=5.


Implement Sorting Using PageRequest.of()

Sorting can be implemented using the Sort class and incorporating it into the PageRequest.of() method.

Add Sorting to Your Controller

Update the controller to accept sorting parameters:

@GetMapping
public Page<User> listUsers(@RequestParam(required = false) String name, 
                            @RequestParam int page, 
                            @RequestParam int size, 
                            @RequestParam String sort, 
                            @RequestParam String direction) {
    Sort sortOrder = direction.equalsIgnoreCase("asc") ? Sort.by(sort).ascending() : Sort.by(sort).descending();
    Pageable pageable = PageRequest.of(page, size, sortOrder);

    return name != null 
        ? userRepository.findByNameContaining(name, pageable) 
        : userService.getUsers(pageable);
}

Clients can now request /users?page=0&size=5&sort=name&direction=asc to retrieve users sorted by name in ascending order.


Advanced: Dynamic Filtering with Specification or Querydsl

Dynamic Filtering with Specifications

For flexible filtering, use the JPA Specifications API. Create a Specification for the User entity:

public class UserSpecification {

    public static Specification<User> hasName(String name) {
        return (root, query, criteriaBuilder) -> 
            name != null ? criteriaBuilder.like(root.get("name"), "%" + name + "%") : null;
    }
    
    public static Specification<User> hasAgeGreaterThan(Integer age) {
        return (root, query, criteriaBuilder) -> 
            age != null ? criteriaBuilder.greaterThan(root.get("age"), age) : null;
    }
}

Integrate this into a dynamic query:

public Page<User> getFilteredUsers(String name, Integer age, Pageable pageable) {
    return userRepository.findAll(Specification.where(UserSpecification.hasName(name))
                                               .and(UserSpecification.hasAgeGreaterThan(age)), pageable);
}

Call this from your controller to support queries like /users?name=John&age=30&page=0&size=5.

Querydsl Alternative

For more complex filters, you can use Querydsl, a powerful library for fluent query construction.


Return Clean Paginated Response

The default Page object from Spring Data JPA provides a lot of metadata like total pages, total elements, and more. Here’s how you can extract that data for a clean response:

@GetMapping
public Map<String, Object> getPaginatedUsers(
        @RequestParam int page, 
        @RequestParam int size, 
        @RequestParam String sort, 
        @RequestParam String direction) {
    
    Sort sortOrder = direction.equalsIgnoreCase("asc") ? Sort.by(sort).ascending() : Sort.by(sort).descending();
    Pageable pageable = PageRequest.of(page, size, sortOrder);
    Page<User> pageResult = userService.getUsers(pageable);

    Map<String, Object> response = new HashMap<>();
    response.put("users", pageResult.getContent());
    response.put("currentPage", pageResult.getNumber());
    response.put("totalItems", pageResult.getTotalElements());
    response.put("totalPages", pageResult.getTotalPages());

    return response;
}

Example response:

{
  "users": [ ... ],
  "currentPage": 0,
  "totalItems": 100,
  "totalPages": 20
}

Optional: Custom Pagination Metadata DTO

To decouple metadata from the data payload, create a custom response DTO:

public class PaginatedResponse<T> {
    private List<T> data;
    private int currentPage;
    private long totalItems;
    private int totalPages;

    // Constructor, Getters, Setters
}

Update your service layer to return the PaginatedResponse DTO:

public PaginatedResponse<User> getUsersWithMetadata(Pageable pageable) {
    Page<User> users = userRepository.findAll(pageable);
    return new PaginatedResponse<>(
        users.getContent(),
        users.getNumber(),
        users.getTotalElements(),
        users.getTotalPages()
    );
}

The flexibility of this approach improves the separation of concerns and keeps your response clean and organized.


Final Thoughts

Combining pagination, filtering, and sorting in a Spring Boot REST API provides clients with powerful tools to query data efficiently. By leveraging Spring Data JPA’s Pageable interface, the flexibility of specifications, and clean response formatting, you can implement these features with ease. Intermediate and advanced developers can further explore dynamic queries using Querydsl or other advanced techniques.

Get started with these techniques and take your REST APIs to the next level!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *