DEV Community

Dev Cookies
Dev Cookies

Posted on

Pagination and Filtering Spring Boot

Let’s design a detailed Spring Boot project focused only on*:
➡ **Filtering
*
Sorting
Pagination
with an H2 in-memory database, following best practices (package structure, DTOs, service layer, repository layer, etc.).


🌟 Project Summary

We’ll build a REST API to manage Book entities that supports:
🔹 Filtering by author and title
🔹 Sorting by title, author, or published date
🔹 Pagination (page number, size)


🏗 Standard Folder Structure

src/main/java/com/example/bookapi
├── BookApiApplication.java
├── controller
│   └── BookController.java
├── dto
│   └── BookDTO.java
├── entity
│   └── Book.java
├── repository
│   └── BookRepository.java
├── service
│   └── BookService.java
└── exception
    └── GlobalExceptionHandler.java

src/main/resources
├── application.properties
└── data.sql  (for sample data)
Enter fullscreen mode Exit fullscreen mode

application.properties

# H2 DB config
spring.datasource.url=jdbc:h2:mem:bookdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.h2.console.enabled=true

# JPA
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
Enter fullscreen mode Exit fullscreen mode

💾 Entity

package com.example.bookapi.entity;

import jakarta.persistence.*;
import lombok.*;

import java.time.LocalDate;

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Book {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    private String author;

    private LocalDate publishedDate;
}
Enter fullscreen mode Exit fullscreen mode

📦 DTO

package com.example.bookapi.dto;

import lombok.*;

import java.time.LocalDate;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BookDTO {
    private Long id;
    private String title;
    private String author;
    private LocalDate publishedDate;
}
Enter fullscreen mode Exit fullscreen mode

📂 Repository

package com.example.bookapi.repository;

import com.example.bookapi.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface BookRepository extends JpaRepository<Book, Long>, JpaSpecificationExecutor<Book> {
}
Enter fullscreen mode Exit fullscreen mode

✅ We use JpaSpecificationExecutor for dynamic filtering.


Service

package com.example.bookapi.service;

import com.example.bookapi.dto.BookDTO;
import com.example.bookapi.entity.Book;
import com.example.bookapi.repository.BookRepository;
import jakarta.persistence.criteria.Predicate;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;

    public Page<BookDTO> getBooks(String title, String author, int page, int size, String sortBy, String sortDir) {
        Specification<Book> spec = (root, query, cb) -> {
            List<Predicate> predicates = new ArrayList<>();
            if (title != null && !title.isEmpty()) {
                predicates.add(cb.like(cb.lower(root.get("title")), "%" + title.toLowerCase() + "%"));
            }
            if (author != null && !author.isEmpty()) {
                predicates.add(cb.like(cb.lower(root.get("author")), "%" + author.toLowerCase() + "%"));
            }
            return cb.and(predicates.toArray(new Predicate[0]));
        };

        Sort sort = sortDir.equalsIgnoreCase("desc") ? Sort.by(sortBy).descending() : Sort.by(sortBy).ascending();
        Pageable pageable = PageRequest.of(page, size, sort);

        return bookRepository.findAll(spec, pageable)
                .map(this::convertToDTO);
    }

    private BookDTO convertToDTO(Book book) {
        return BookDTO.builder()
                .id(book.getId())
                .title(book.getTitle())
                .author(book.getAuthor())
                .publishedDate(book.getPublishedDate())
                .build();
    }
}
Enter fullscreen mode Exit fullscreen mode

🎮 Controller

package com.example.bookapi.controller;

import com.example.bookapi.dto.BookDTO;
import com.example.bookapi.service.BookService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
@CrossOrigin(origins = "http://localhost:3000") // If you plan frontend integration
public class BookController {

    private final BookService bookService;

    @GetMapping
    public Page<BookDTO> getBooks(
            @RequestParam(required = false) String title,
            @RequestParam(required = false) String author,
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "5") int size,
            @RequestParam(defaultValue = "title") String sortBy,
            @RequestParam(defaultValue = "asc") String sortDir
    ) {
        return bookService.getBooks(title, author, page, size, sortBy, sortDir);
    }
}
Enter fullscreen mode Exit fullscreen mode

Global Exception Handler

package com.example.bookapi.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public ResponseEntity<?> handleException(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
    }
}
Enter fullscreen mode Exit fullscreen mode

📝 Sample Data (data.sql)

INSERT INTO book (title, author, published_date) VALUES ('Clean Code', 'Robert C. Martin', '2008-08-01');
INSERT INTO book (title, author, published_date) VALUES ('Effective Java', 'Joshua Bloch', '2008-05-08');
INSERT INTO book (title, author, published_date) VALUES ('Spring in Action', 'Craig Walls', '2018-04-10');
INSERT INTO book (title, author, published_date) VALUES ('Java Concurrency in Practice', 'Brian Goetz', '2006-05-19');
INSERT INTO book (title, author, published_date) VALUES ('Head First Design Patterns', 'Eric Freeman', '2004-10-25');
Enter fullscreen mode Exit fullscreen mode

🚀 Run

GET http://localhost:8080/api/books?author=martin&page=0&size=2&sortBy=publishedDate&sortDir=desc
Enter fullscreen mode Exit fullscreen mode

🏁 Summary

✅ Full pagination + filtering + sorting with:
Clean layering (Controller → Service → Repo)
DTO usage
Specification API for filtering
H2 + sample data


Let’s build a minimal but complete React frontend** for the Spring Boot filtering + sorting + pagination API.

Features:

  • Display paginated, filtered, and sorted book data
  • Search by title & author
  • Sort by title, author, or published date
  • Pagination controls

🏗 Folder Structure

book-frontend/
├── public/
│   └── index.html
└── src/
    ├── components/
    │   └── BookList.js
    ├── App.js
    ├── index.js
    └── App.css
Enter fullscreen mode Exit fullscreen mode

1️⃣ Install React App

npx create-react-app book-frontend
cd book-frontend
npm install axios
Enter fullscreen mode Exit fullscreen mode

2️⃣ BookList.js

import React, { useEffect, useState } from "react";
import axios from "axios";

const BookList = () => {
  const [books, setBooks] = useState([]);
  const [titleFilter, setTitleFilter] = useState("");
  const [authorFilter, setAuthorFilter] = useState("");
  const [sortBy, setSortBy] = useState("title");
  const [sortDir, setSortDir] = useState("asc");
  const [page, setPage] = useState(0);
  const [size, setSize] = useState(5);
  const [totalPages, setTotalPages] = useState(0);

  useEffect(() => {
    fetchBooks();
  }, [titleFilter, authorFilter, sortBy, sortDir, page, size]);

  const fetchBooks = () => {
    axios
      .get("http://localhost:8080/api/books", {
        params: {
          title: titleFilter,
          author: authorFilter,
          sortBy,
          sortDir,
          page,
          size,
        },
      })
      .then((res) => {
        setBooks(res.data.content);
        setTotalPages(res.data.totalPages);
      })
      .catch((err) => console.error(err));
  };

  const handlePageChange = (newPage) => {
    if (newPage >= 0 && newPage < totalPages) {
      setPage(newPage);
    }
  };

  return (
    <div className="container">
      <h1>📚 Book List</h1>

      <div className="filters">
        <input
          type="text"
          placeholder="Filter by title"
          value={titleFilter}
          onChange={(e) => {
            setPage(0);
            setTitleFilter(e.target.value);
          }}
        />
        <input
          type="text"
          placeholder="Filter by author"
          value={authorFilter}
          onChange={(e) => {
            setPage(0);
            setAuthorFilter(e.target.value);
          }}
        />
        <select value={sortBy} onChange={(e) => setSortBy(e.target.value)}>
          <option value="title">Title</option>
          <option value="author">Author</option>
          <option value="publishedDate">Published Date</option>
        </select>
        <select value={sortDir} onChange={(e) => setSortDir(e.target.value)}>
          <option value="asc">Ascending</option>
          <option value="desc">Descending</option>
        </select>
        <select value={size} onChange={(e) => setSize(parseInt(e.target.value))}>
          <option value="2">2</option>
          <option value="5">5</option>
          <option value="10">10</option>
        </select>
      </div>

      <table border="1" cellPadding="10" style={{ marginTop: "10px" }}>
        <thead>
          <tr>
            <th>Title</th>
            <th>Author</th>
            <th>Published Date</th>
          </tr>
        </thead>
        <tbody>
          {books.length > 0 ? (
            books.map((book) => (
              <tr key={book.id}>
                <td>{book.title}</td>
                <td>{book.author}</td>
                <td>{book.publishedDate}</td>
              </tr>
            ))
          ) : (
            <tr>
              <td colSpan="3">No books found</td>
            </tr>
          )}
        </tbody>
      </table>

      <div className="pagination" style={{ marginTop: "10px" }}>
        <button onClick={() => handlePageChange(page - 1)} disabled={page === 0}>
          Previous
        </button>
        <span style={{ margin: "0 10px" }}>
          Page {page + 1} of {totalPages}
        </span>
        <button
          onClick={() => handlePageChange(page + 1)}
          disabled={page + 1 >= totalPages}
        >
          Next
        </button>
      </div>
    </div>
  );
};

export default BookList;
Enter fullscreen mode Exit fullscreen mode

3️⃣ App.js

import React from "react";
import "./App.css";
import BookList from "./components/BookList";

function App() {
  return (
    <div className="App">
      <BookList />
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

4️⃣ App.css

.container {
  padding: 20px;
  max-width: 800px;
  margin: auto;
}

.filters input,
.filters select {
  margin-right: 10px;
  padding: 5px;
}
Enter fullscreen mode Exit fullscreen mode

5️⃣ index.js

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<App />);
Enter fullscreen mode Exit fullscreen mode

🛡 Enable CORS in Spring Boot (if not already done)

✅ Already in BookController:

@CrossOrigin(origins = "http://localhost:3000")
Enter fullscreen mode Exit fullscreen mode

🚀 Run Everything

Spring Boot:

mvn spring-boot:run
Enter fullscreen mode Exit fullscreen mode

http://localhost:8080/h2-console (for checking data)


React:

npm start
Enter fullscreen mode Exit fullscreen mode

➡ React will open at http://localhost:3000


What you’ll see

✅ A working book list
✅ Filter books by title or author
✅ Change sort field and direction
✅ Adjust page size
✅ Navigate pages


Top comments (0)