DEV Community

Cover image for Cursor-Based Pagination in NestJS with TypeORM πŸš€
Juan Castillo
Juan Castillo

Posted on

2

Cursor-Based Pagination in NestJS with TypeORM πŸš€

Hey there, devs! πŸ‘‹ If you've ever struggled with paginating large datasets efficiently, you're in the right place. Today, we'll implement cursor-based pagination in a NestJS API using TypeORM. This approach is far superior to offset-based pagination when dealing with large databases. Let's dive in! πŸŠβ€β™‚οΈ

What We'll Cover πŸ”₯

  • Using a createdAt cursor to fetch records efficiently.
  • Implementing a paginated endpoint in NestJS.
  • Returning data with a cursor for the next page.

1️⃣ Creating a DTO for Pagination Parameters

First, let's define a DTO to handle pagination parameters:

import { IsOptional, IsString, IsNumber } from 'class-validator';
import { Transform } from 'class-transformer';

export class CursorPaginationDto {
  @IsOptional()
  @IsString()
  cursor?: string; // Receives the `createdAt` of the last item on the previous page

  @IsOptional()
  @Transform(({ value }) => parseInt(value, 10))
  @IsNumber()
  limit?: number = 10; // Number of items per page (default: 10)
}
Enter fullscreen mode Exit fullscreen mode

2️⃣ Implementing the Query in the Service

Now, let's create the logic in our service:

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { CursorPaginationDto } from './dto/cursor-pagination.dto';

@Injectable()
export class UserService {
  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
  ) {}

  async getUsers(cursorPaginationDto: CursorPaginationDto) {
    const { cursor, limit } = cursorPaginationDto;

    const queryBuilder = this.userRepository
      .createQueryBuilder('user')
      .orderBy('user.createdAt', 'DESC')
      .limit(limit + 1); // Fetching one extra record to check if there's a next page

    if (cursor) {
      queryBuilder.where('user.createdAt < :cursor', { cursor });
    }

    const users = await queryBuilder.getMany();

    const hasNextPage = users.length > limit;
    if (hasNextPage) {
      users.pop(); // Remove the extra item
    }

    const nextCursor = hasNextPage ? users[users.length - 1].createdAt : null;

    return {
      data: users,
      nextCursor,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Creating the Controller

Finally, let's expose our paginated endpoint:

import { Controller, Get, Query } from '@nestjs/common';
import { UserService } from './user.service';
import { CursorPaginationDto } from './dto/cursor-pagination.dto';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get()
  async getUsers(@Query() cursorPaginationDto: CursorPaginationDto) {
    return this.userService.getUsers(cursorPaginationDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Defining the Database Model

Here's our User entity:

import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn } from 'typeorm';

@Entity()
export class User {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column()
  name: string;

  @CreateDateColumn()
  createdAt: Date;
}
Enter fullscreen mode Exit fullscreen mode

How Cursor-Based Pagination Works ⚑

1️⃣ The first request to GET /users does not include a cursor. It fetches the first limit records.

2️⃣ The backend returns a nextCursor, which is the createdAt timestamp of the last user in the response.

3️⃣ To fetch the next page, the frontend makes a request to GET /users?cursor=2024-03-09T12:34:56.000Z, and the backend will return users created before that timestamp.

4️⃣ This process continues until nextCursor is null, meaning there are no more records left.


Example JSON Response πŸ“

{
  "data": [
    { "id": "1", "name": "John", "createdAt": "2024-03-09T12:00:00.000Z" },
    { "id": "2", "name": "Anna", "createdAt": "2024-03-09T11:45:00.000Z" }
  ],
  "nextCursor": "2024-03-09T11:45:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Why Use Cursor-Based Pagination? πŸ€”

βœ… Better Performance: Avoids OFFSET, which slows down large datasets.

βœ… Scalability: Works seamlessly with millions of records.

βœ… Optimized Queries: Using indexed fields like createdAt makes queries lightning-fast. ⚑


Conclusion 🎯

Cursor-based pagination is a game-changer for handling large datasets in APIs. πŸš€ It's faster, more efficient, and ensures a smoother experience for your users. Now you’re ready to implement it in your own NestJS project! πŸ’ͺ

Got questions or improvements? Drop them in the comments! πŸ’¬ Happy coding! πŸ˜ƒ

Tiugo image

Modular, Fast, and Built for Developers

CKEditor 5 gives you full control over your editing experience. A modular architecture means you get high performance, fewer re-renders and a setup that scales with your needs.

Start now

Top comments (0)

Billboard image

Try REST API Generation for MS SQL Server.

DreamFactory generates live REST APIs from database schemas with standardized endpoints for tables, views, and procedures in OpenAPI format. We support on-prem deployment with firewall security and include RBAC for secure, granular security controls.

See more!

πŸ‘‹ Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay