📄 How to paginate your MongoDB queries in TypeScript

📄 How to paginate your MongoDB queries in TypeScript

🤔 What is pagination

Here is a quick reminder on what is pagination and why it is useful.

Pagination is the fact of dividing a list of data into chunks.

It is useful in web development because it allows to fetch only a decent number of entities to be displayed, and access to next page only when scrolling down or selecting next page, thus improving drastically performance.

Moreover, it gives the frontend access to some logic such as page size, or sorting, which is interesting for us lazy backend developers.

🤩 How to do it with TypeScript and MongoDB

The only prequisite for this tutorial is to use Mongoose for MongoDB object modeling.

Dependencies

Add the following dependencies to your project :

npm install mongoose-paginate-v2
npm install --save-dev @types/mongoose-paginate-v2

Mongoose-paginate-v2 is the best package I could find to perform pagination with mongoose. This package is a mongoose plugin.

Adding a mongoose plugin

Adding a mongoose plugin is pretty straightforward :

import mongoosePaginate from 'mongoose-paginate-v2'
...
 await mongoose.connect(process.env["MONGODB_URI"])
 mongoose.plugin(mongoosePaginate)

Just register the plugin after connecting to you mongoDB cluster with mongoose, and the pagination plugin is now plugged.

Setting up your database entity

This is where it gets tricky. Since the package mongoose-paginate-v2 overlaps some mongoose type definitions, we need to find a workaround to make it work. After some hours trying, here is what I end up with :

import * as mongoose from 'mongoose'
import {Authority} from '../../../domain/models/auth/Authority'
import mongoosePaginate from 'mongoose-paginate-v2'
import {Document, PaginateModel} from 'mongoose'

export interface DBAccount
    extends mongoose.Document {
    id: string,
    email: string,
    password: string,
 }

export const AccountSchema = new mongoose.Schema({
    email: { type: String },
    password: { type: String }
}).plugin(mongoosePaginate)

interface DBAccountModel<T extends Document> extends PaginateModel<T> {}

export const DBAccountModel: DBAccountModel<DBAccount> = mongoose.model<DBAccount>(
    'DBAccount',
    AccountSchema
) as DBAccountModel<DBAccount>

By doing those weird transformations, the exported DBAccountModel is now able to use pagination. Dont ask me how it works, but if you find better way to do this, I definitely want to know in comments ⬇️

Let's paginate

Here is the cool part. To paginate your requests, you only have to give some options from PaginateOptions. As an example, here is a simple query paginated:

async findAll(sort: string, page: number, size: number): Promise<PaginateResult<DBAccount>> {
   return DBAccountModel.paginate({}, { sort: sort, page: page, limit: size })
}

You are good to go ! A lot of parameters are available in options for paginations.

Most interesting ones are :

  • sort: specify a field and an order to sort your results
  • page: give the page to display
  • limit: the size of the page

Bonus points

If you want to makea cleaner implementation, I suggest you to add a top entity for pagination parameters (Pageable), and a top entity for page results (Page). Then, write a mapper to map Pageable to PaginateOptions, and PaginateResult to Page. This way, your business logic does not depend on mongoose way to handle pagination, you will only manipulate Page and Pageable. This way, you respect the Liskov substitution principle (L from SOLID).

As for me, I made my own Page, Pageable, and mappers to get closer to the entities from Java Spring Boot pagination.

You are good to go !

I hope this article will be helpful to someone, thanks for reading !