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)
⚙ 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
💾 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;
}
📦 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;
}
📂 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> {
}
✅ 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();
}
}
🎮 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);
}
}
⚠ 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());
}
}
📝 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');
🚀 Run
Access H2 console: http://localhost:8080/h2-console
➡ JDBC URL:jdbc:h2:mem:bookdb
Example API call:
GET http://localhost:8080/api/books?author=martin&page=0&size=2&sortBy=publishedDate&sortDir=desc
🏁 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
1️⃣ Install React App
npx create-react-app book-frontend
cd book-frontend
npm install axios
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;
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;
4️⃣ App.css
.container {
padding: 20px;
max-width: 800px;
margin: auto;
}
.filters input,
.filters select {
margin-right: 10px;
padding: 5px;
}
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 />);
🛡 Enable CORS in Spring Boot (if not already done)
✅ Already in BookController
:
@CrossOrigin(origins = "http://localhost:3000")
🚀 Run Everything
Spring Boot:
mvn spring-boot:run
➡ http://localhost:8080/h2-console (for checking data)
React:
npm start
➡ 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)