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! ๐Ÿ˜ƒ

MongoDB Atlas runs apps anywhere. Try it now.

MongoDB Atlas runs apps anywhere. Try it now.

MongoDB Atlas lets you build and run modern apps anywhereโ€”across AWS, Azure, and Google Cloud. With availability in 115+ regions, deploy near users, meet compliance, and scale confidently worldwide.

Start Free

Top comments (3)

Collapse
 
juan_castillo profile image
Juan Castillo โ€ข

Hi @victorsfranco Great question!

Yes, to go back to the previous page when using cursor-based pagination, you need to implement a bit of reverse logic.

Here's how it usually works:

Store the cursors on the client side: For each page you load, save both the nextCursor and prevCursor (if provided). Think of it like a breadcrumb trail.

Use a flag (e.g., isPrevious: true): When requesting the previous page, you send the prevCursor along with a flag that tells the server you want to go backward.

Reverse the query on the backend: The backend should apply the cursor with a reversed comparison (e.g., createdAt < cursor instead of createdAt > cursor), and also reverse the sort order temporarily to fetch the correct set of records.

Reverse the result before sending: Since you fetched in reverse order, reverse the result before returning it so it appears in the correct chronological order.

This way, you can paginate both forward and backward using cursors without relying on offset-based logic, which can become unreliable with dynamic data.

Image description

Collapse
 
victorsfranco profile image
Victor Franco โ€ข

I just implemented it on my API and its working fine. Thank you!

Collapse
 
victorsfranco profile image
Victor Franco โ€ข โ€ข Edited

I got the point, but I have one question: In this example, I could understand how I can go to next page, but how can I came back to previus page? Should I additionaly apply something as a previus cursor, writing the reverse logic?

Thank you for this valuable and useful content!

Warp.dev image

Warp is the highest-rated coding agentโ€”proven by benchmarks.

Warp outperforms every other coding agent on the market, and gives you full control over which model you use. Get started now for free, or upgrade and unlock 2.5x AI credits on Warp's paid plans.

Download Warp

๐Ÿ‘‹ Kindness is contagious

Explore this insightful piece, celebrated by the caring DEV Community. Programmers from all walks of life are invited to contribute and expand our shared wisdom.

A simple "thank you" can make someoneโ€™s dayโ€”leave your kudos in the comments below!

On DEV, spreading knowledge paves the way and fortifies our camaraderie. Found this helpful? A brief note of appreciation to the author truly matters.

Letโ€™s Go!