Server-Side Pagination with PrimeNG p-table Lazy Loading
Load only the current page from your backend using PrimeNG p-table lazy loading, with implementation steps and verification checks.
31 May 2026, 10:02 UTC

Problem and takeaway
Loading an entire dataset into an Angular table forces the browser to parse and render every row at once. With thousands of records, the first paint slows down and sorting or filtering happens client-side on the full collection.
Use PrimeNG p-table with lazy loading enabled. The table then asks your backend for only the current page, applying pagination, sorting, and filtering parameters on the server. This guide covers setup, implementation, verification, and error handling.
Desired outcome
A p-table that displays one page at a time, sends first, rows, sort, and filter parameters to an API, shows a loading overlay while fetching, and updates the paginator from the backend total count.
Prerequisites
- An Angular application with PrimeNG installed. Confirm
primengappears inpackage.json. - A backend endpoint that accepts pagination and sort parameters and returns the current page rows plus an accurate total count.
- The
LazyLoadEventtype fromprimeng/api. - Version assumption:
onLazyLoadandLazyLoadEventbehavior described here matches PrimeNG 12 through 16. PrimeNG 13+ and 16+ changed standalone imports and theming APIs, so verify the exact import path for your installed version before copying code.
Procedure
1. Install and import the table module
Run this in the project root to install PrimeNG if it is missing:
npm install primeng --save
No special permissions are required beyond write access to the project directory. Risk: installing a major version that changes import paths; check the release notes for your target version.
In module-based applications, add TableModule to the module imports:
import { NgModule } from '@angular/core';
import { TableModule } from 'primeng/table';
@NgModule({
imports: [TableModule]
})
export class AppModule {}
In standalone applications, import TableModule in the component imports array. The exact path can differ between PrimeNG versions; confirm it against your installed package.
2. Add the table template
<p-table
[value]='customers'
[lazy]='true'
[paginator]='true'
[rows]='10'
[totalRecords]='totalRecords'
[loading]='loading'
(onLazyLoad)='loadCustomers($event)'>
<ng-template pTemplate='header'>
<tr>
<th>Name</th>
<th>Email</th>
</tr>
</ng-template>
<ng-template pTemplate='body' let-customer>
<tr>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
</tr>
</ng-template>
</p-table>
The [lazy]='true' binding tells PrimeNG not to expect the full dataset in [value]. Instead, it emits onLazyLoad whenever the user changes pages, sorts, or filters.
3. Implement the lazy load handler
import { Component } from '@angular/core';
import { LazyLoadEvent } from 'primeng/api';
export class CustomersComponent {
customers: any[] = [];
totalRecords = 0;
loading = false;
errorMessage = '';
constructor(private customerService: CustomerService) {}
loadCustomers(event: LazyLoadEvent): void {
this.loading = true;
this.errorMessage = '';
const params = {
first: event.first ?? 0,
rows: event.rows ?? 10,
sortField: event.sortField,
sortOrder: event.sortOrder ?? 1,
filters: this.mapFilters(event.filters)
};
this.customerService.getPage(params).subscribe({
next: (response) => {
this.customers = response.data;
this.totalRecords = response.totalCount;
this.loading = false;
},
error: () => {
this.loading = false;
this.errorMessage = 'Unable to load customers. Please retry.';
}
});
}
private mapFilters(filters: Record | undefined): any {
// Transform PrimeNG filter metadata into your backend query format.
return filters ?? {};
}
}
LazyLoadEvent is the object PrimeNG passes to the handler. It contains the requested page offset, page size, sort field, sort direction, and filter state. Map those fields to whatever query parameters your backend expects.
4. Add the data service
import { HttpClient, HttpParams } from '@angular/common/http';
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CustomerService {
private apiUrl = 'https://api.example.com/customers';
constructor(private http: HttpClient) {}
getPage(params: {
first: number;
rows: number;
sortField?: string;
sortOrder?: number;
filters?: any;
}) {
let httpParams = new HttpParams()
.set('first', String(params.first))
.set('rows', String(params.rows));
if (params.sortField) {
httpParams = httpParams.set('sortField', params.sortField);
httpParams = httpParams.set('sortOrder', String(params.sortOrder ?? 1));
}
return this.http.get<{ data: any[]; totalCount: number }>(
this.apiUrl,
{ params: httpParams }
);
}
}
Expected checks
- Open browser DevTools to the Network tab.
- Change the table page, sort a column, or apply a filter.
- Confirm exactly one XHR or fetch request is sent with
first,rows, and sort parameters. - Confirm the PrimeNG loading overlay appears while
[loading]is true. - Confirm the paginator total matches the backend
totalCount.
Recovery and error handling
This change modifies client state only; there is no database rollback. If the HTTP call fails:
- Set
loadingto false so the overlay disappears. - Keep
totalRecordsunchanged so the paginator does not jump. - Show an error message and provide a retry button that calls
loadCustomerswith the sameLazyLoadEventparameters. - If the new behavior causes regressions, revert the component and template changes and redeploy the previous version.
Limitations
- Do not assign the full dataset to
[value]when[lazy]is true. PrimeNG expects only the rows for the current page. - The backend must return an accurate total count. Incorrect totals break paginator math and last-page behavior.
LazyLoadEvent.filterscontains nested filter objects. Map them to your backend query format before calling the API.- Rapid filter edits can trigger repeated requests. Debounce filter input and cancel pending HTTP requests, for example with
switchMaporAbortController, to avoid race conditions. - PrimeNG import paths and standalone APIs changed after version 12 and again in version 16. Verify your installed version before adapting this code.
Practical verification
- Build and serve the application locally.
- Open DevTools Network, filter by XHR or fetch.
- Click page two in the table paginator.
- Check that the request URL includes
first=10androws=10, assuming ten rows per page. - Sort a column and confirm the request includes
sortFieldandsortOrder. - Compare the paginator's displayed total with the backend
totalCountvalue.
If any check fails, inspect the component handler to confirm it reads first, rows, sortField, sortOrder, and filters from the LazyLoadEvent argument.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.