Business Scenario
In this lab, we will enhance the existing Medicare Doctor Search API by implementing pagination and sorting alongside the filtering features developed in the previous lab.
Pre-Lab Preparation
Phase 7 — Pagination & Sorting
What problem are we solving?
1
Module:
1) Additional Components in Project
GET /api/doctors10 doctors 100 doctors 1,000 doctors 50,000 doctors
Database - 50,000 Doctors Request page=0 size=6 -- Only 6 Doctors returned
Instead :
Understand Pagination
2
Page 0 → Doctors 1–6 Page 1 → Doctors 7–12 Page 2 → Doctors 13–18 Page 3 → Doctors 19–24 Page 4 → Doctor 25
Understand the Pagination Classes
3
We'll mainly use:
Represents:
Which page? How many records? How should records be sorted?
Creates that pagination request.
Doctor records + Current page + Page size + Total elements + Total pages + Whether last page
contains:
Add Pagination Response DTO
4
dto/ └── DoctorPageResponse.java
package com.medicare.dto;
import java.util.List;
public class DoctorPageResponse { private List<DoctorResponse> doctors;
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
public DoctorPageResponse(
List<DoctorResponse> doctors,
int pageNumber,
int pageSize,
long totalElements,
int totalPages,
boolean last) {
this.doctors = doctors;
this.pageNumber = pageNumber;
this.pageSize = pageSize;
this.totalElements = totalElements;
this.totalPages = totalPages;
this.last = last;
}
public List<DoctorResponse> getDoctors() {
return doctors;
}
public int getPageNumber() {
return pageNumber;
} public int getPageSize() {
return pageSize;
}
public long getTotalElements() {
return totalElements;
}
public int getTotalPages() {
return totalPages;
}
public boolean isLast() {
return last;
}
}Update DoctorProfileRepository
5
package com.medicare.repository;
import java.math.BigDecimal;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import com.medicare.entity.DoctorProfile;
public interface DoctorProfileRepository extends JpaRepository<DoctorProfile, Long> {
boolean existsByUserId(Long userId);
@Query(""" SELECT d
FROM DoctorProfile d
JOIN d.user u
WHERE
(:specialization IS NULL
OR LOWER(d.specialization) = LOWER(:specialization))
AND
(:maxFee IS NULL
OR d.consultationFee <= :maxFee)
AND
(:name IS NULL
OR LOWER(u.name)
LIKE CONCAT(CONCAT('%', LOWER(:name)), '%'))
""") LIKE CONCAT(CONCAT('%', LOWER(:name)), '%'))
""")
Page<DoctorProfile> searchDoctors(
@Param("specialization")
String specialization,
@Param("maxFee")
BigDecimal maxFee,
@Param("name")
String name,
Pageable pageable
);
}How Pageable Works With Repository
6
Pageable pageable = PageRequest.of(page,size,sort
doctorRepository.searchDoctors( specialization,maxFee,name,pageable);
Filtering + Sorting + Limit + Offset + Total count
Decide Our Sorting Options
7
or: ?sortBy=somethingThatDoesNotExist
name specialization experienceYears consultationFee
Update DoctorService
8
public DoctorPageResponse searchDoctors(
String specialization,
BigDecimal maxFee,
String name,
int page,
int size,
String sortBy,
String direction) {
specialization = normalizeText(specialization);
name = normalizeText(name);
validatePagination(page,size);
if (maxFee != null && maxFee.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException(
"Maximum consultation fee must be greater than zero"
);
} String sortField = resolveSortField(sortBy);
Sort.Direction sortDirection = resolveSortDirection(direction);
Sort sort = Sort.by(sortDirection,sortField);
Pageable pageable = PageRequest.of(page,size,sort);
Page<DoctorProfile> doctorPage = doctorRepository.searchDoctors(
specialization, maxFee, name, pageable);
List<DoctorResponse> doctorResponses = new ArrayList<>();
for (DoctorProfile doctor : doctorPage.getContent()) {
doctorResponses.add( mapToDoctorResponse(doctor));
}
return new DoctorPageResponse(
doctorResponses,
doctorPage.getNumber(),
doctorPage.getSize(),
doctorPage.getTotalElements(),
doctorPage.getTotalPages(),
doctorPage.isLast()
);
}import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import com.medicare.dto.DoctorPageResponse;Add Pagination Validation
9
private void validatePagination( int page, int size) {
if (page < 0) {
throw new IllegalArgumentException( "Page number cannot be negative");
} if (size < 1 || size > 50) {
throw new IllegalArgumentException("Page size must be between 1 and 50");
}
}Because otherwise somebody could request: ?size=1000000 which defeats the entire purpose of pagination.
Add Sort Field Validation
10
private String resolveSortField(
String sortBy) {
if (sortBy == null || sortBy.isBlank()) { return "consultationFee";
}
return switch (sortBy) {
case "name" -> "user.name";
case "specialization" -> "specialization";
case "experienceYears" -> "experienceYears";
case "consultationFee" -> "consultationFee";
default -> throw new IllegalArgumentException("Invalid sort field");
};
}Validate Sorting Direction
11
private Sort.Direction resolveSortDirection(String direction) {
if (direction == null || direction.isBlank() || direction.equalsIgnoreCase("asc")) {
return Sort.Direction.ASC;
}
if (direction.equalsIgnoreCase("desc")) { return Sort.Direction.DESC;
}
throw new IllegalArgumentException( "Sort direction must be asc or desc");
}asc ASC Asc desc DESC
up down abc
Updated DoctorService Imports
12
Update DoctorController
13
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;@GetMapping
public ResponseEntity<List<DoctorResponse>>searchDoctors(...)@GetMapping
public ResponseEntity<DoctorPageResponse> searchDoctors(
@RequestParam(required = false)
String specialization,
@RequestParam(required = false)
BigDecimal maxFee,
@RequestParam(required = false)
String name,
@RequestParam(defaultValue = "0")
int page,
@RequestParam(defaultValue = "6")
int size,
@RequestParam(defaultValue = "consultationFee")
String sortBy,
@RequestParam(defaultValue = "asc")
String direction) { DoctorPageResponse response = doctorService.searchDoctors(
specialization, maxFee, name, page, size, sortBy, direction
);
return ResponseEntity.ok(response);
}Our Default Request
14
page = 0 size = 6 sortBy = consultationFee direction = asc
Test Basic Pagination
15
GE - http://localhost:8080/api/doctors?page=0&size=3
{
"doctors": [
{
"id": 2,
"name": "Dr. Rohan Mehta",
"specialization": "Dentist",
"experienceYears": 5,
"consultationFee": 700.00,
"photoUrl": "https://example.com/rohan.jpg"
},
{
"id": 3,
"name": "Dr. Priya Nair",
"specialization": "General Physician",
"experienceYears": 10,
"consultationFee": 900.00,
"photoUrl": "https://example.com/priya.jpg"
},
{
"id": 5,
"name": "Dr. Sneha Kulkarni",
"specialization": "Dermatologist",
"experienceYears": 7,
"consultationFee": 1000.00,
"photoUrl": "https://example.com/sneha.jpg"
} ], "pageNumber": 0, "pageSize": 3, "totalElements": 8, "totalPages": 3, "last": false }
GET - http://localhost:8080/api/doctors?page=0&size=3
{
"doctors": [
...
],
"pageNumber": 1,
"pageSize": 3,
"totalElements": 8,
"totalPages": 3,
"last": false
}GET /api/doctors?page=2&size=3
{
"pageNumber": 2,
"pageSize": 3,
"totalElements": 8,
"totalPages": 3,
"last": true
}Create DoctorResponse
16
GET - /api/doctors?sortBy=consultationFee&direction=asc
₹500 ₹700 ₹900 ₹1000
GET - /api/doctors?sortBy=consultationFee&direction=desc
₹1000 ₹900 ₹700 ₹500
Sort by Experience
17
GET /api/doctors?sortBy=experienceYears&direction=desc
Sort by Doctor Name
18
GET /api/doctors?sortBy=name&direction=asc
Filters + Pagination Together
19
GET /api/doctors?specialization=Cardiologist&page=0&size=6
Find Cardiologists ↓ Paginate Results ↓ Return First 6
Filters + Sorting + Pagination
20
GET /api/doctors?specialization=Cardiologist&maxFee=1500&name=ananya&page=0&size=6&sortBy=consultationFee&direction=asc
Test Invalid Page
21
GET /api/doctors?page=-1
400 BAD REQUESTTest Invalid Page Size
22
GET /api/doctors?size=0
400 BAD REQUESTTest Invalid Sort Property
23
GET /api/doctors?sortBy=randomField
400 BAD REQUESTInvalid sort field
Test Invalid Direction
24
GET /api/doctors?direction=up
400 BAD REQUESTSort direction must be asc or desc
What If Page Doesn't Exist?
25
GET /api/doctors?page=20&size=6
{
"doctors": [],
"pageNumber": 20,
"pageSize": 6,
"totalElements": 8,
"totalPages": 2,
"last": true
}We are done with this lab. The latest source code has been uploaded to GitHub. You can access the latest commit using the link below:
Great job!
You have successfully implemented pagination and sorting in the Medicare Doctor Search API using Spring Data JPA.
Checkpoint
Git Push
git push origin branchNameNext-Lab Preparation
Module:
1) Mastering Springboot Security