Implementing API Pagination with Node.js,Express and Mongoose. An Example with 1 million records.

This article Implements API pagination with nodejs, mongoose. An example with 1 million records. Consider that your application has a million records and users fetching the data. Can the server handle a million DB records in a single GET request?. How can you handle GET requests if you have a million records in your database?.

Well, that's where pagination comes to the rescue. Here we will replicate the production scenario, such as handling a million records and see how to implement different pagination methods. Let's implement API pagination with nodejs, mongoose

Wesbos Nodejs Course

Implementing pagination with nodejs mongoose

Implementing%20API%20Pagination%20with%20Node%20js,Express%20a%20b873b4295a904083905f1a58bfcb281e/Screenshot_2021-04-18_at_9.57.24_PM.png

Types of pagination

In general, There are two types of pagination. they are,

  • Offset based pagination
  • Cursor based pagination

Offset based pagination

It is one of the most common pagination methods we are using for decades. It simply uses limit and offset in SQL queries to paginate the data from database.

In NOSQL database, it will be limit and skip

1SELECT * FROM users
2ORDER BY timestamp
3OFFSET 10
4LIMIT 5

Complete source code is available in the GitHub repo

Let's implement it in our application and see the advantages/disadvantages of it. Implementation for offset pagination is straightforward,

1const fetchCompanies = async (req, res) => {
2 try {
3 const limit = parseInt(req.query.limit)
4 const offset = parseInt(req.query.skip)
5
6 const tradesCollection = await Trades.find()
7 .skip(offset)
8 .limit(limit)
9 const tradesCollectionCount = await Trades.count()
10
11 const totalPages = Math.ceil(tradesCollectionCount / limit)
12 const currentPage = Math.ceil(tradesCollectionCount % offset)
13
14 res.status(200).send({
15 data: tradesCollection,
16 paging: {
17 total: tradesCollectionCount,
18 page: currentPage,
19 pages: totalPages,
20 },
21 })
22 } catch (e) {
23 console.log("Error", e)
24 res.status(500).send({
25 data: null,
26 })
27 }
28}

An important line here is,

1const tradesCollection = await Trades.find()
2 .skip(offset)
3 .limit(limit)

MongoDB has skip and limit operators to implement offset based pagination. On sending the response, it is recommended to send them along with pagination data.

1res.status(200).send({
2 data: tradesCollection,
3 paging: {
4 total: tradesCollectionCount,
5 page: currentPage,
6 pages: totalPages,
7 },
8})

Demo

https://youtu.be/hw6K-XR3o6Q

Drawbacks of Offset based pagination

  • Offset pagination doesn't scale for large datasets. Using SQL offset or NOSQL skip operators. It scans the record one by one and skip or offset it. If your database has a million records, just like we see in this tutorial, offset-based pagination can affect scalability.
  • If you have real-time data, offset-based pagination will be unreliable and problematic. There will be skipping of data or duplicate data. Read more

Cursor based pagination

Cursor-based pagination uses a unique record as a cursor for the fetch. When we pass a cursor and limit, it gets all the data that are less than the cursor value along with the limit. Implementing cursor based pagination with nodejs,mongoose

The important thing here is the cursor value should be sequential or timestamps. In that way, we can use comparison operators to fetch the data.

Implementing%20API%20Pagination%20with%20Node%20js,Express%20a%20b873b4295a904083905f1a58bfcb281e/cursor_based.png

Before getting into the coding part of it, let's do a simple walk-through on cursor-based pagination. Let's say the limit is 8, and the user is making a request.

When it comes for the first time, there will be no cursor value, and it fetches the most recent value.

Note: Here, we use time as a cursor value which is in descending order.

DB call fetches 8+1 value from DB because we need the 9th value as a cursor for the next fetch. Then, we can send the cursor value along with the next request. we need to compare that cursor value and fetch data that are less than the cursor

Implementing%20API%20Pagination%20with%20Node%20js,Express%20a%20b873b4295a904083905f1a58bfcb281e/cursor_based_update.png

Let's see the implementation of cursor-based pagination.

1const limit = parseInt(req.query.limit)
2const cursor = req.query.cursor
3
4let decryptedCursor
5let tradesCollection
6if (cursor) {
7 decryptedCursor = decrypt(cursor)
8
9 let decrypedDate = new Date(decryptedCursor * 1000)
10
11 tradesCollection = await Trades.find({
12 time: {
13 $lt: new Date(decrypedDate),
14 },
15 })
16 .sort({ time: -1 })
17 .limit(limit + 1)
18 .exec()
19} else {
20 tradesCollection = await Trades.find({})
21 .sort({ time: -1 })
22 .limit(limit + 1)
23}
24
25const hasMore = tradesCollection.length === limit + 1
26
27let nextCursor = null
28if (hasMore) {
29 const nextCursorRecord = tradesCollection[limit]
30
31 var unixTimestamp = Math.floor(nextCursorRecord.time.getTime() / 1000)
32
33 nextCursor = encrypt(unixTimestamp.toString())
34 tradesCollection.pop()
35}
36
37res.status(200).send({
38 data: tradesCollection,
39 paging: {
40 hasMore,
41 nextCursor,
42 },
43})

Here, we check if it's the first request or not based on the cursor value. If the request has a cursor as a query param, we fetch the data based on that.

Note : We encrypt the cursor value for security purpose. it's recommended to encrypt the cursor value before sending them as response(for security)

If it has the cursor, we use it in our DB query with a comparison operator,

1tradesCollection = await Trades.find({
2 time: {
3 $lt: new Date(decrypedDate),
4 },
5})
6 .sort({ time: -1 })
7 .limit(limit + 1)
8 .exec()

Then, we can find if our database has more data based on the following condition.

1const hasMore = tradesCollection.length === limit + 1

Since, we fetch limit + 1 data, we can find out that it has more data if our fetched data and limit + 1 are same.

If it has more value, we need to determine the next cursor value. we can do that using,

1if (hasMore) {
2 const nextCursorRecord = tradesCollection[limit]
3
4 var unixTimestamp = Math.floor(nextCursorRecord.time.getTime() / 1000)
5
6 nextCursor = encrypt(unixTimestamp.toString())
7 tradesCollection.pop()
8}

Another critical thing to note here is removing the last element from the fetched data. Because we needed that value to calculate the cursor, it shouldn't be for the end-user results.

Demo

https://youtu.be/1KhLGqtjaco

Further Reading

Evolving API Pagination at Slack

Learning Node

Course

To Read More

Modern React Redux Toolkit - Login ...

User Authentication is one of the common workflow in web applications. In this t...

Building Nodejs Microservice - A Cl...

This Article explains everything about how to build Nodejs Microservices in clou...

I Accidentally wiped the entire dat...

One of the tragic accident in my job turned out to be good learning for me in re...