When we browse list pages with abundant infomation, we usually implement pagination techniques to enahnce the user experience on a website or an app. I learned about the pagination which requires the first page, the number of items per request, the last page, and the user’s current position. I regard it as an academic study on pagination, and I wonder how real-life services provide and use pagination techniques. Here, I would like to introduce the cursor pagination technique
What is cursor pagination?
The traditional pagination system is commonly referred to as Page-based pagination which I mentioned above. Before we begin, let’s assume that all the item IDs are sorted in numerically to better illustrate the issue at hand. So, what exactly is Cursor-based pagination?
Page-based VS Cursor-based
Page-based pagination(PBP) retrieves data by pages. Let’s say there are 100 items to fetch. If you set 20 items per request, you will have 5 pages. On the other hand, Cursor-based pagination(CBP) requires the last item’s ID from your previous request. If the last item’s ID was 20, your next request will start from 21.
The main weakness of PBP is that if new data is inserted while you are navigating from page 2 to page 3, you might see some of the page 2 data on page 3 due to shifting items. Conversely, if some data has been deleted while you are navigating, you might skip certain items that were supposed to be displayed on page 3
CBP overcomes this issue by keeping track of the last item’s ID. If more items need to be fetched, the next request will start from the ID following the last item. This ensures that no items are missed in the display.
Now, let’s explore how we can implement CBP in a NestJS project.
The following article assumes basic knowledge of Typeorm, Postman and NestJS. If you need further clarification, feel free to ask.
Basic Implementation of CBP
Because pagination usually involves a SELECT query, which often requires information such as “how many items you want to retrieve” and “what order to use”, you can use a DTO to include this information.
Let’s say we are running Netflix-like service, and the client system wants to retrieve movies in descending order by id, the DTO should include both id and order . So, we need to create CursorPaginationDto
export class CursorPaginationDto {
@IsInt()
id: number;
@IsArray()
@IsIn(['ASC', 'DESC'])
order: 'ASC' | 'DESC' = 'DESC';
}
JavaScript
복사
CursorPaginationDto
Our goal is to set descending order as the default when the client request items, wether in descending or ascending order. After creating DTO, we need a controller to handle API requests. Let’s create a MovieController that handles GET requests at the URL ( http://localhost:3000/movie )
@Controller('movie')
export class MovieController{
constructor(private readonly movieService: MovieService){}
@Get()
getMovies(@Query() dto: CursorPaginationDto) {
return this.movieService.findAll(dto, userId);
}
}
JavaScript
복사
MovieController.getMovies()
Before working on the movieService.findAll() method, we first need to define, how to create the selectQueryBuilder when both id and order are provided in the CursorPaginationDTO.
import { SelectQueryBuilder } from 'typeorm';
async applyCursorPaginationParamsToQb(qb: SelectQueryBuilder<T>, dto: CursorPaginationDto) {
const { id, order } = dto;
const direction = order === 'ASC' ? '>' : '<';
qb.where(`${}`
}
JavaScript
복사