0%

Create a Paging API in Spring Boot

September 24, 2025

Springboot

1. Returning Page From CrudRepository

In EventRepository I created the following signature:

import dev.james.alicetimetable.commons.database.entities.Event
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import java.util.UUID

interface EventRepository : CrudRepository<Event, Int> {
    ...

    @Query("""
        select e from Event e
        order by e.createdAt desc
    """)
    fun findByPageAndLimit(pageable: Pageable): Page<Event>
}

We also define a wrapper data class for consumption of frontend:

data class EventsWithTotal(
    val events: List<EventDTO>,
    val total: Long
)

now we combine everything to get:

1@Service
2class EventQueryApplicationService(
3    private val eventRepository: EventRepository
4) {
5    fun getEvents(page: Int, limit: Int): EventsWithTotal {
6        val pageable = PageRequest.of(page, limit)
7        val eventPage = eventRepository.findByPageAndLimit(pageable)
8        val result = EventsWithTotal(events = eventPage.content.map { it.toDTO() },
9                                     total = eventPage.totalElements)
10        return result
11    }
12}

Here we have returned two results to frontend to accomplish pagination:

  • In line 8 we return the list of paged elements to the frontend.

  • In line 9 we return the total number of all matching rows to frontend so that it can show the number of pages.

2. Frontend

2.1. The Paging Result

2.2. Component for Pagination
2.2.1. Implementation

In the following we use some of the elements from shadcn:

import {
    Pagination,
    PaginationContent,
    PaginationEllipsis,
    PaginationItem,
    PaginationLink,
    PaginationNext,
    PaginationPrevious,
} from '@/components/ui/pagination';

/**
 *  There are plenty of messy logic on determining how numbers are shown, can ignore it.
 */
const CustomPagination = (props: {
    consecutivePagesBlockSize: number;
    currentPageIndex: number;
    totalPages: number;
    onPageIndexChange: (page: number) => void;
}) => {
    const { consecutivePagesBlockSize, currentPageIndex, totalPages, onPageIndexChange } = props;
    const availablePageNumbers = Array.from({ length: totalPages }, (_, i) => i);
    const lastPageIndex = totalPages - 1;
    const consecutivePagesBlockStartIndex = Math.max(currentPageIndex - 1, 0);
    const approachesTheEnd = lastPageIndex - consecutivePagesBlockStartIndex <= consecutivePagesBlockSize - 1;
    const consecutivePagesBlock = availablePageNumbers.slice(
        approachesTheEnd ? lastPageIndex - (consecutivePagesBlockSize - 1) : consecutivePagesBlockStartIndex,
        consecutivePagesBlockStartIndex + consecutivePagesBlockSize
    );

    const forceDisplayPageOne = currentPageIndex >= consecutivePagesBlockSize - 1;
    const forceDisplayLast = lastPageIndex - currentPageIndex >= consecutivePagesBlockSize - 1;
    if (totalPages <= consecutivePagesBlockSize) {
        return (
            <>
                <Pagination>
                    <PaginationContent>
                        <PaginationItem>
                            <PaginationPrevious
                                href="#"
                                onClick={() => onPageIndexChange(Math.max(currentPageIndex - 1, 0))}
                            />
                        </PaginationItem>
                        {availablePageNumbers.map(page => {
                            const isActive = page === currentPageIndex;
                            return (
                                <PaginationItem onClick={() => onPageIndexChange(page)}>
                                    <PaginationLink href="#" isActive={isActive}>
                                        {page + 1}
                                    </PaginationLink>
                                </PaginationItem>
                            );
                        })}
                        <PaginationItem>
                            <PaginationNext
                                href="#"
                                onClick={() => onPageIndexChange(Math.min(currentPageIndex + 1, totalPages - 1))}
                            />
                        </PaginationItem>
                    </PaginationContent>
                </Pagination>
            </>
        );
    } else {
        return (
            <>
                <Pagination>
                    <PaginationContent>
                        <PaginationItem>
                            <PaginationPrevious
                                href="#"
                                onClick={() => onPageIndexChange(Math.max(currentPageIndex - 1, 0))}
                            />
                        </PaginationItem>
                        {forceDisplayPageOne && (
                            <>
                                <PaginationItem onClick={() => onPageIndexChange(0)}>
                                    <PaginationLink href="#" isActive={currentPageIndex === 0}>
                                        1
                                    </PaginationLink>
                                </PaginationItem>
                                {currentPageIndex >= 3 && totalPages > consecutivePagesBlockSize + 1 && (
                                    <PaginationItem>
                                        <PaginationEllipsis />
                                    </PaginationItem>
                                )}
                            </>
                        )}
                        {consecutivePagesBlock.map(page => {
                            const isActive = page === currentPageIndex;
                            return (
                                <PaginationItem onClick={() => onPageIndexChange(page)}>
                                    <PaginationLink href="#" isActive={isActive}>
                                        {page + 1}
                                    </PaginationLink>
                                </PaginationItem>
                            );
                        })}
                        {lastPageIndex - currentPageIndex >= consecutivePagesBlockSize &&
                            totalPages > consecutivePagesBlockSize + 1 && (
                                <PaginationItem>
                                    <PaginationEllipsis />
                                </PaginationItem>
                            )}
                        {forceDisplayLast && (
                            <PaginationItem>
                                <PaginationLink
                                    href="#"
                                    isActive={currentPageIndex === lastPageIndex}
                                    onClick={() => onPageIndexChange(lastPageIndex)}
                                >
                                    {lastPageIndex + 1}
                                </PaginationLink>
                            </PaginationItem>
                        )}

                        <PaginationItem>
                            <PaginationNext
                                href="#"
                                onClick={() => onPageIndexChange(Math.min(currentPageIndex + 1, totalPages - 1))}
                            />
                        </PaginationItem>
                    </PaginationContent>
                </Pagination>
            </>
        );
    }
};

export default CustomPagination;
2.2.2. Usage

In my logging page I simply define

export default function Logging() {
    const [page, setPage] = useState(0);
    const { data: loggings, isLoading } = 
        eventApi.endpoints.getEvents.useQuery({ page, limit: LIMIT });

    return (
            <div>
                <CustomPagination
                    consecutivePagesBlockSize={3}
                    currentPageIndex={page}
                    totalPages={Math.ceil((loggings?.total || 0) / LIMIT)}
                    onPageIndexChange={page => {
                        setPage(page);
                    }}
                />
            ...