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!