Pagination with Angular and Spring Boot
Implementing pagination in an application using Angular for the frontend and Spring Boot for the backend application typically involves both server-side logic,
where the backend (Spring Boot) application handles the data retrieval and pagination logic, and the frontend (Angular) logic that displays the paginated data and provides controls for navigating between pages.You can use Spring Boot Data JPA Pageable when you have lots of data in the server and want to distribute data to be displayed on several pages while displaying on the UI (User Interface) instead of displaying all data in a single scrollable page. This pagination layout will give users better experience when they want to see less amount of data in a page without having to scroll down at bottom.
The Spring Boot Data JPA provides a useful Pageable interface that provides a quite number of methods for building pagination in the UI side. Even you can sort the data returned by the JPA query using the methods provided in this interface API.
Benefits of Pagination
Performance Optimization
- Only a subset of data is loaded at a time, minimizing initial load time.
- The browser handles fewer DOM elements, hence lower memory usage and improving responsiveness.
- Backend queries can be limited to specific pages, reducing the server strain.
Improved User Experience
- Users can quickly jump between pages without waiting for the entire dataset to load.
- Breaking content into manageable chunks makes it easier to read.
- Avoids long scrolls, on the page, that can be frustrating on mobile or low-powered devices.
Developer Control & Flexibility
- Angular’s component-based architecture allows for reusable pagination controls.
- Ready-to-use pagination components (Angular Material) with styling and accessibility built-in.
- Easily tie pagination to observables (Reactive programmable) and services for dynamic updates.
Security & Scalability
- Limits exposure of sensitive data by fetching only what’s needed.
- Works well with APIs, databases, and cloud services for enterprise-level apps.
Real-World Use Cases
- Browsing products page by page in e-commerce websites.
- Viewing logs, users, or reportsin admin dashboard.
- Loading posts incrementally in social media feeds.
Server-Side (Spring Boot) Implementation
RESTful API with Pagination Parameters
Create a Spring Boot REST controller that accepts pagination parameters such as page (current page number), size (number of items per page), and optionally sort (sorting criteria).
@RestController
@CrossOrigin(origins = "*")
public class TutorialRestController {
@Autowired
private TutorialService tutorialService;
@GetMapping("/tutorials")
public Page<TutorialDto> tutorials(Pageable pageable) {
return tutorialService.paginatedTutorials(pageable);
}
} The controller method should return a Page object, which encapsulates the list of items for the current page, total pages, total elements, and other pagination metadata.
Pageable Interface
Utilize Spring Data JPA’s Pageable interface in your repository method to automatically handle database queries with pagination.
@Service
public class TutorialService {
@Autowired
private TutorialRepository tutorialRepository;
public Page<TutorialDto> paginatedTutorials(Pageable pageable) {
Page<Tutorial> tutorials = tutorialRepository.findAll(pageable);
Page<TutorialDto> paginatedTutorials = tutorials.map(entity -> {
TutorialDto tutorialDto = EntityDtoConverter.toDto(entity);
return tutorialDto;
});
return paginatedTutorials;
}
} Client-Side (Angular) Implementation
Service
Create an Angular service to interact with your Spring Boot API endpoint (/tutorials). This service will make HTTP requests to fetch data, passing the current page and size as query parameters.
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class TutorialService {
private baseUrl = 'http://localhost:8080/';
constructor(private http: HttpClient) { }
getPaginatedTutorials(page: number, itemsPerPage: number): Observable<any> {
const params = new HttpParams()
.set('page', page.toString())
.set('size', itemsPerPage.toString());
return this.http.get<any>(this.baseUrl + 'tutorials', { params });
}
} Component
In your Angular component, manage the current page, page size, and subscribe to the data service to fetch data.
import { Component, signal, OnInit } from '@angular/core';
import { TutorialService } from './tutorial-service';
import { Tutorial } from './tutorial';
@Component({
selector: 'app-root',
templateUrl: './app.html',
standalone: false,
styleUrl: './app.css'
})
export class App implements OnInit {
protected readonly title = signal('Angular Pagination');
tutorials: Tutorial[] = [];
currentPage: number = 0;
totalPages: number = 0;
itemsPerPage: number = 10;
totalItems: number = 0;
pageSizeOptions: number[] = [5, 10, 20, 50];
constructor(private tutorialService: TutorialService) { }
ngOnInit(): void {
this.retrieveTutorials();
}
retrieveTutorials(): void {
this.tutorialService.getPaginatedTutorials(this.currentPage, this.itemsPerPage).subscribe(
(data) => {
this.tutorials = data.content;
this.totalPages = data.totalPages;
this.currentPage = data.pageable.pageNumber;
this.totalItems = data.totalItems;
},
(error) => {
console.log(error);
}
);
}
goToPage(page: number): void {
this.currentPage = page;
this.retrieveTutorials();
}
nextPage(): void {
if (this.currentPage < this.totalPages - 1) {
this.currentPage++;
this.retrieveTutorials();
}
}
previousPage(): void {
if (this.currentPage > 0) {
this.currentPage--;
this.retrieveTutorials();
}
}
changePageSize(size: number): void {
this.itemsPerPage = size;
this.currentPage = 0;
this.retrieveTutorials();
}
onPageChange(page: number): void {
this.currentPage = page;
this.retrieveTutorials();
}
get pageNumbers(): number[] {
return Array.from({ length: this.totalPages }, (_, index) => index);
}
getVisiblePages(): number[] {
const delta = 2; // Number of pages to show on each side of current page
let start = Math.max(1, this.currentPage - delta);
let end = Math.min(this.totalPages, this.currentPage + delta);
// Ensure we always show at least 5 pages if available
if (end - start < 4) {
if (start === 1) {
end = Math.min(this.totalPages, start + 4);
} else if (end === this.totalPages) {
start = Math.max(1, end - 4);
}
}
const pages = [];
for (let i = start - 1; i <= end; i++) {
pages.push(i);
}
return pages;
}
} Displaying Data
Iterate over the received data and display it in your component’s template.
<table class="tutorials-table">
<thead>
<tr>
<th>Id</th>
<th>Content</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let t of tutorials" class="tutorial-row">
<td class="tutorial-id">{{ t.id }}</td>
<td class="tutorial-content">{{ t.content }}</td>
</tr>
</tbody>
</table> Pagination UI
Implement pagination controls (e.g., “Previous,” “Next,” page numbers) that update the current page and trigger a new data fetch from the server.
<div class="pagination-controls">
<span>Page {{ currentPage + 1 }} of {{ totalPages }}</span>
<button class="pagination-btn" (click)="goToPage(0)" [disabled]="currentPage === 0">First</button>
<button class="pagination-btn" (click)="previousPage()" [disabled]="currentPage === 0">Previous</button>
<button *ngFor="let page of getVisiblePages()" class="pagination-btn page-number" [class.active]="(page) === currentPage" (click)="goToPage(page)"> {{ page + 1 }} </button>
<button class="pagination-btn" (click)="nextPage()" [disabled]="currentPage === totalPages - 1">Next</button>
<button class="pagination-btn" (click)="goToPage(totalPages - 1)" [disabled]="currentPage === totalPages - 1">Last</button>
</div>
<div class="page-size-selector">
<label for="pageSize">Items per page:</label>
<select
id="pageSize"
(change)="changePageSize($any($event.target).value)"
class="page-size-select">
<option *ngFor="let size of pageSizeOptions" [value]="size" [selected]="size === itemsPerPage"> {{size}} </option>
</select>
<label for="pageNo">Go to page:</label>
<select id="pageNo" (change)="goToPage($any($event.target).value)" class="page-size-select">
<option *ngFor="let page of pageNumbers" [value]="page" [selected]="page === currentPage">{{ page + 1 }}</option>
</select>
</div> When the user interacts with the pagination controls in Angular, the component updates the page and size values.
These values are then passed to the Angular service, which constructs the HTTP request with the appropriate query parameters.
The Spring Boot backend receives these parameters, uses Pageable to fetch the correct subset of data from the database, and returns a Page object.
The Angular component receives this Page object, updates its data display, and refreshes the pagination controls based on the total pages and elements.
Final Output
Finally your Spring Boot server application and Angular client application are up and running. So, you will see the following output on the browser:

You can change the number of items to be displayed on the page, you can even go to a particular page by selecting the page number in the dropdown box and this page number in the dropdown box will be in sync when you click on any page number link.
No comments