Pagination
All GET_LIST endpoints in NestJS CRUD Automator automatically support pagination with configurable limits and page numbers.
Basic Pagination
Paginate results using query parameters:
GET /users?limit=10&page=1
This returns the first 10 users.
Pagination Parameters
limit
Number of items per page (default: 10, max: typically 100):
GET /users?limit=20
page
Page number (1-based indexing):
GET /users?page=2
Response Structure
Paginated responses include metadata:
{
items: UserEntity[], // Array of results
total: number, // Total count of all items
page: number, // Current page number
limit: number, // Items per page
totalPages: number, // Total number of pages
hasNextPage: boolean, // Whether next page exists
hasPreviousPage: boolean // Whether previous page exists
}
Example response:
{
"items": [
{
"id": "uuid-1",
"username": "john_doe",
"email": "john@example.com"
},
{
"id": "uuid-2",
"username": "jane_doe",
"email": "jane@example.com"
}
],
"total": 50,
"page": 1,
"limit": 10,
"totalPages": 5,
"hasNextPage": true,
"hasPreviousPage": false
}
Combining with Filters and Sorting
Pagination works seamlessly with filtering and sorting:
GET /users?limit=20&page=2&username[operator]=cont&username[value]=john&orderBy=createdAt&orderDirection=DESC
This returns page 2 of users with “john” in username, sorted by creation date descending, 20 items per page.
Programmatic Usage
Use pagination in service calls:
import { Injectable } from "@nestjs/common";
import { UserService } from "./user.service";
import { IApiGetListResponseResult } from "@elsikora/nestjs-crud-automator";
import { UserEntity } from "./user.entity";
@Injectable()
export class UserReportService {
constructor(private readonly userService: UserService) {}
async getAllUsers(): Promise<UserEntity[]> {
const allUsers: UserEntity[] = [];
let page = 1;
const limit = 100;
let hasMore = true;
while (hasMore) {
const result: IApiGetListResponseResult<UserEntity> =
await this.userService.getList(
{
where: {},
limit,
page,
},
{}
);
allUsers.push(...result.items);
hasMore = result.hasNextPage;
page++;
}
return allUsers;
}
}
Configuring Pagination Limits
Set default and maximum pagination limits in controller:
@ApiController<UserEntity>({
entity: UserEntity,
routes: {
[EApiRouteType.GET_LIST]: {
pagination: {
defaultLimit: 20,
maxLimit: 100,
},
},
},
})
Cursor-Based Pagination
For large datasets, implement cursor-based pagination using custom methods:
import { Injectable } from "@nestjs/common";
import { ApiService, ApiServiceBase } from "@elsikora/nestjs-crud-automator";
import { UserEntity } from "./user.entity";
import { MoreThan } from "typeorm";
@Injectable()
@ApiService<UserEntity>({
entity: UserEntity,
})
export class UserService extends ApiServiceBase<UserEntity> {
// ... constructor
async getUsersAfter(
cursor: string,
limit: number = 20
): Promise<{ items: UserEntity[]; nextCursor: string | null }> {
const users = await this.repository.find({
where: {
id: MoreThan(cursor),
},
order: {
id: "ASC",
},
take: limit + 1, // Fetch one extra to check if more exist
});
const hasMore = users.length > limit;
const items = hasMore ? users.slice(0, limit) : users;
const nextCursor = hasMore ? items[items.length - 1].id : null;
return { items, nextCursor };
}
}
Infinite Scroll
Implement infinite scroll patterns:
class UserListComponent {
users: UserEntity[] = [];
page = 1;
limit = 20;
loading = false;
hasMore = true;
async loadMore(): Promise<void> {
if (this.loading || !this.hasMore) return;
this.loading = true;
try {
const response = await fetch(
`/users?limit=${this.limit}&page=${this.page}`
);
const data = await response.json();
this.users.push(...data.items);
this.hasMore = data.hasNextPage;
this.page++;
} finally {
this.loading = false;
}
}
}
Performance Considerations
Offset vs Cursor Pagination
Offset Pagination (default):
- Pros: Simple to implement, supports jumping to any page
- Cons: Performance degrades with large offsets
- Best for: Small to medium datasets, when page jumping is needed
Cursor Pagination:
- Pros: Consistent performance regardless of position
- Cons: Cannot jump to arbitrary pages
- Best for: Large datasets, infinite scroll, real-time data
Counting Performance
Total counts can be expensive on large tables. Consider:
- Caching counts:
@Injectable()
export class UserService extends ApiServiceBase<UserEntity> {
private countCache: { value: number; timestamp: number } | null = null;
private readonly CACHE_TTL = 60000; // 1 minute
async getList(properties, relations) {
const now = Date.now();
if (
!this.countCache ||
now - this.countCache.timestamp > this.CACHE_TTL
) {
const total = await this.repository.count();
this.countCache = { value: total, timestamp: now };
}
// Use cached count
const items = await this.repository.find({
skip: (properties.page - 1) * properties.limit,
take: properties.limit,
});
return {
items,
total: this.countCache.value,
page: properties.page,
limit: properties.limit,
};
}
}
- Approximate counts for very large tables:
// PostgreSQL approximate count
const result = await this.repository.query(`
SELECT reltuples::bigint AS estimate
FROM pg_class
WHERE relname = 'users'
`);
const approximateTotal = result[0].estimate;
Index Optimization
Ensure pagination queries are fast with proper indexes:
@Entity("users")
@Index(["createdAt", "id"]) // Composite index for pagination with sorting
export class UserEntity {
@PrimaryGeneratedColumn("uuid")
id: string;
@Column({ type: "timestamp", default: () => "CURRENT_TIMESTAMP" })
createdAt: Date;
}
Empty Results
Handle empty result sets gracefully:
{
"items": [],
"total": 0,
"page": 1,
"limit": 10,
"totalPages": 0,
"hasNextPage": false,
"hasPreviousPage": false
}
Next Steps
- Filtering and Sorting - Filter paginated results
- Relations - Paginate with related entities
- Core Concepts - DTOs - Pagination DTO structure