Overview

DTOs and helpers for both offset-based and cursor-based pagination.

Features

  • Offset pagination DTO
  • Cursor pagination DTO
  • Prisma helpers
  • Response builders

Installation

npx hanma add pagination --framework nest

Code

import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
import { IsOptional, IsInt, Min, Max, IsString } from "class-validator";
import { Type } from "class-transformer";

// Offset-based pagination
export class PaginationQueryDto {
  @ApiPropertyOptional({ minimum: 1, default: 1 })
  @Type(() => Number)
  @IsOptional()
  @IsInt()
  @Min(1)
  page?: number = 1;

  @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 20 })
  @Type(() => Number)
  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(100)
  limit?: number = 20;
}

export class PaginationMeta {
  @ApiProperty()
  total: number;

  @ApiProperty()
  page: number;

  @ApiProperty()
  limit: number;

  @ApiProperty()
  totalPages: number;

  @ApiProperty()
  hasNext: boolean;

  @ApiProperty()
  hasPrev: boolean;
}

export class PaginatedResponseDto<T> {
  data: T[];
  meta: PaginationMeta;

  constructor(data: T[], total: number, page: number, limit: number) {
    const totalPages = Math.ceil(total / limit);

    this.data = data;
    this.meta = {
      total,
      page,
      limit,
      totalPages,
      hasNext: page < totalPages,
      hasPrev: page > 1,
    };
  }
}

// Cursor-based pagination
export class CursorPaginationQueryDto {
  @ApiPropertyOptional()
  @IsOptional()
  @IsString()
  cursor?: string;

  @ApiPropertyOptional({ minimum: 1, maximum: 100, default: 20 })
  @Type(() => Number)
  @IsOptional()
  @IsInt()
  @Min(1)
  @Max(100)
  limit?: number = 20;
}

export class CursorPaginationMeta {
  @ApiProperty()
  hasMore: boolean;

  @ApiPropertyOptional()
  nextCursor?: string;

  @ApiPropertyOptional()
  prevCursor?: string;
}

export class CursorPaginatedResponseDto<T> {
  data: T[];
  meta: CursorPaginationMeta;

  constructor(
    data: T[],
    hasMore: boolean,
    nextCursor?: string,
    prevCursor?: string
  ) {
    this.data = data;
    this.meta = { hasMore, nextCursor, prevCursor };
  }
}

// Utility functions
export function encodeCursor(id: string | number, createdAt?: Date): string {
  const payload = { id, createdAt: createdAt?.toISOString() };
  return Buffer.from(JSON.stringify(payload)).toString("base64url");
}

export function decodeCursor(cursor: string): { id: string; createdAt?: Date } {
  try {
    const payload = JSON.parse(Buffer.from(cursor, "base64url").toString());
    return {
      id: payload.id,
      createdAt: payload.createdAt ? new Date(payload.createdAt) : undefined,
    };
  } catch {
    throw new Error("Invalid cursor");
  }
}

// Calculate skip for Prisma/TypeORM
export function getSkip(page: number, limit: number): number {
  return (page - 1) * limit;
}

// Prisma pagination helper
export function prismaOffsetPagination(query: PaginationQueryDto) {
  const page = query.page || 1;
  const limit = query.limit || 20;

  return {
    skip: getSkip(page, limit),
    take: limit,
  };
}

// Prisma cursor pagination helper
export function prismaCursorPagination(query: CursorPaginationQueryDto) {
  const limit = query.limit || 20;

  if (!query.cursor) {
    return { take: limit + 1 }; // Take extra to check hasMore
  }

  const { id } = decodeCursor(query.cursor);

  return {
    take: limit + 1,
    skip: 1, // Skip the cursor itself
    cursor: { id },
  };
}

// Usage:
/*
@Controller("users")
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  async findAll(@Query() query: PaginationQueryDto) {
    const { data, total } = await this.usersService.findAll(query);
    return new PaginatedResponseDto(data, total, query.page, query.limit);
  }

  @Get("cursor")
  async findAllCursor(@Query() query: CursorPaginationQueryDto) {
    const { data, hasMore, nextCursor } = await this.usersService.findAllCursor(query);
    return new CursorPaginatedResponseDto(data, hasMore, nextCursor);
  }
}
*/

Usage

const { data, total } = await this.service.findAll(query);
return new PaginatedResponseDto(data, total, query.page, query.limit);

Dependencies

No external dependencies required.

Frequently Asked Questions

How do I install Pagination Helpers?
Run the following command: npx hanma add pagination --framework nest
How do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to src/utils/pagination.ts.
Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.