Skip to Content

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:

user.controller.ts
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:

user.controller.ts
@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:

user.service.ts
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:

frontend-example.ts
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:

  1. 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, }; } }
  1. 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

Last updated on