Offset Pagination

page and limit

Never return a whole table. Clients ask for one page: ?page=2&limit=20. The server uses skip = (page - 1) * limit and take = limit, and returns metadata.

Example

The math, plus the meta object the client needs to draw pagination buttons:

function pageMeta(page: number, limit: number, total: number) {
  const totalPages = Math.max(1, Math.ceil(total / limit));
  return {
    skip: (page - 1) * limit,
    take: limit,
    meta: { page, limit, total, totalPages, hasNext: page < totalPages, hasPrev: page > 1 },
  };
}

console.log(pageMeta(1, 20, 137));
console.log(pageMeta(7, 20, 137));
console.log(pageMeta(1, 20, 0));

Output

{
  skip: 0,
  take: 20,
  meta: {
    page: 1,
    limit: 20,
    total: 137,
    totalPages: 7,
    hasNext: true,
    hasPrev: false
  }
}
{
  skip: 120,
  take: 20,
  meta: {
    page: 7,
    limit: 20,
    total: 137,
    totalPages: 7,
    hasNext: false,
    hasPrev: true
  }
}
{
  skip: 0,
  take: 20,
  meta: {
    page: 1,
    limit: 20,
    total: 0,
    totalPages: 1,
    hasNext: false,
    hasPrev: false
  }
}

A Reusable Query DTO

src/common/dto/pagination-query.dto.ts

import { Type } from 'class-transformer';
import { IsInt, IsOptional, Max, Min } from 'class-validator';

export class PaginationQueryDto {
  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  page: number = 1;

  @IsOptional()
  @Type(() => Number)
  @IsInt()
  @Min(1)
  @Max(100)                 // protect the database from ?limit=1000000
  limit: number = 20;
}

Note: Query strings are always strings. @Type(() => Number) plus transform: true on the ValidationPipe converts them before validation.