API with NestJS #47. Implementing pagination with MongoDB and Mongoose

JavaScript MongoDB NestJS TypeScript

This entry is part 47 of 177 in the API with NestJS

When our application grows, so does the database. At some point, we might return a lot of data from our endpoints. It might prove to be too much for our frontend application to process, for example. Therefore, we might need to paginate our records by returning just a portion of them. This article explores different ways of achieving that with MongoDB and Mongoose and considers their pros and cons.

You can find the code from this article in this repository.

Using skip and limit

The most straightforward form of pagination is expecting the users to provide how many documents they want to skip. Also, they can declare how many documents they want to receive.

To successfully implement pagination, we need a predictable order of documents. To achieve that, we must sort them:

posts.service.ts

By doing , we sort in an ascending order.

Above, we use an important feature of ids in MongoDB. The id in MongoDB consists of 12 bytes, 4 of them being the timestamp. While doing so, we need to be aware of some disadvantages:

  • the timestamp value is measured in seconds, and documents created within the same second do not have a guaranteed valid order,
  • the id is generated by clients that might have different system clocks.

Sorting by the has a significant advantage because MongoDB creates a unique index on the field. This increases the performance of sorting the documents by .

Implementing the pagination

The first step to implement the above approach is to allow the user to provide the offset and limit through query params. To do that, let’s use the and .

paginationParams.ts

If you want to know more about the and , check out Error handling and data validation and Serializing the response with interceptors.

We can now use the above params in our controller:

posts.controller.ts

Now we can use the above arguments in the  method.

posts.service.ts

Thanks to doing that, the users can now specify how many posts they want to fetch and how many to skip. For example, requesting results in ten posts while omitting the first twenty documents.

Counting the documents

A common approach is to display how many pages of posts we have. To do that, we need to count how many documents we have in the database. To do that, we either need to use the aggregation framework or perform two separate queries.

posts.service.ts

Now in our response, we have both the results and the total number of documents.

Disadvantages

The solution of using the limit and offset is very widely used both with SQL databases and MongoDB. Unfortunately, its performance leaves room for improvement. Using the method still requires the database to scan from the beginning of the collection. First, the database sorts all of the documents by id. Then, MongoDB drops the specified number of documents. This might prove to be quite a lot of work with big collections.

Besides the performance issues, we need to consider consistency. Ideally, the document should appear in results only once. This might not be the case:

  1. the first user fetches page number one with posts,
  2. right after that, the second user creates a new post – after sorting, it ends up on page number one,
  3. the first user fetches the second page.

The user sees the last element of the first page again on the second page. Unfortunately, the user also missed the element that was added to the first page, which is even worse.

Advantages

The approach with the limit and the offset is common and straightforward to implement. Its big advantage is that it is straightforward to skip multiple pages of documents. Also, it is straightforward to change the column used for sorting, including sorting by multiple columns. Therefore, it can be a viable solution if the expected offset is not too big and the inconsistencies are acceptable.

Keyset pagination

If we care a lot about the performance, we might want to look for an alternative to the above approach. One of them is the keyset pagination. Here, we use the fact that the ids in MongoDB consist of timestamps and can be compared:

  1. we fetch a page of documents from the API,
  2. we check the id of the last document,
  3. then, we request documents with ids greater than the id of the last document.

Thanks to the above approach, the database no longer needs to process unnecessary documents. First, let’s create a way for the user to provide the starting id.

paginationParams.ts

Now, we need to use the property in our service.

posts.service.ts

Thanks to doing , the user receives only the posts created after the post with the provided id.

Disadvantages

A big drawback of the keyset pagination is the need to know the exact document we want to start with. We can overcome this issue by combining it with offset-based pagination. Another issue with this approach is that it would be difficult for the user to skip multiple data pages at once.

Advantages

The most significant advantage of the keyset pagination is the performance improvement compared to the offset-based approach. Also, it helps solve the issue of inconsistency where the user adds or removes elements between fetching pages.

Summary

In this article, we’ve compared two types of pagination with MongoDB and Mongoose. We’ve considered both the disadvantages and advantages of the keyset pagination and the offset-based approach. Neither of them are perfect, but combining them covers a lot of different cases. It is crucial to choose the right tool for the given job.

Series Navigation<< API with NestJS #46. Managing transactions with MongoDB and MongooseAPI with NestJS #48. Definining indexes with MongoDB and Mongoose >>
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments