How to paginate with Mongoose in Node.js?
Asked Answered
P

36

327

I am writing a webapp with Node.js and mongoose. How can I paginate the results I get from a .find() call? I would like a functionality comparable to "LIMIT 50,100" in SQL.

Premises answered 4/4, 2011 at 14:27 Comment(2)
Use skip and limit property while finding data from collection.Roadbed
This link may be used for you in details. laxmanchavda.blogspot.com/2018/06/…Amalbergas
P
299

After taking a closer look at the Mongoose API with the information provided by Rodolphe, I figured out this solution:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });
Premises answered 4/4, 2011 at 15:15 Comment(10)
What about "count"? You need that to know how many pages there are.Baynebridge
@ChrisHinkle Why do you say that?Zwickau
Chris Hinkle's explanation why this does not scale: https://mcmap.net/q/98531/-how-to-paginate-with-mongoose-in-node-js .Orv
@ChrisHinkle This seems to be the case with all DBMSs. Lex's comment below the linked answer seems to explain more.Patriliny
This is problematic since you don't get the number of results and hence you can't make proper pagination.Aronow
If I query the collection by geoNear and sort by default distance, how can I avoid to use skip to do pagination?Magnesite
count of total results is required in most of cases to show the pages or total results to user.Carabineer
Answer is Ok. but this approach won't be so fast/efficient on db with large number of documents as it will first go through all of the documents before going to the skip point.Proterozoic
@GeorgeBailey I feel the same, collections having large number of records would have to be traversed a lot. Any other approach you could suggest?Ophidian
@Ophidian yeah. I've used identifier for that. what you do in there is send the last records id back to server and get some records with id greater than the sent. As Id is indexed so, it will be much fasterProterozoic
S
349

I'm am very disappointed by the accepted answers in this question. This will not scale. If you read the fine print on cursor.skip( ):

The cursor.skip() method is often expensive because it requires the server to walk from the beginning of the collection or index to get the offset or skip position before beginning to return result. As offset (e.g. pageNumber above) increases, cursor.skip() will become slower and more CPU intensive. With larger collections, cursor.skip() may become IO bound.

To achieve pagination in a scaleable way combine a limit( ) along with at least one filter criterion, a createdOn date suits many purposes.

MyModel.find( { createdOn: { $lte: request.createdOnBefore } } )
.limit( 10 )
.sort( '-createdOn' )
Sappy answered 13/5, 2014 at 19:48 Comment(19)
But how would you get page two from that query without skip? If you're viewing 10 results per page, and there are a 100 results, how do you then define the offset or skip value? You're not answering the question of pagination, so you can't be 'disappointed', although it is a valid caution. Although the same issue is in MySQL offset,limit. It is has to traverse the tree to the offset before returning results. I'd take this with a grain of salt, if your result sets are less than 1mil and there's no preservable performance hit, use skip().Dissociation
I'm a noob when it comes to mongoose/mongodb, but to answer Lex's question, it appears that, as the results are ordered by '-createdOn', you would replace the value of request.createdOnBefore with the least value of createdOn returned in the previous result set, and then requery.Lingerfelt
@JoeFrambach Requesting based on createdOn seems problematic. Skip was embedded for a reason. The docs are only warning of the performance hit of cycling through the btree index, which is the case with all DBMSs. For the users question "something comparable MySQL to LIMIT 50,100" .skip is exactly right.Dissociation
How would you solve the issue of a user viewing records 31-40, then a new record is inserted by another client, then the user hits next. Now they are viewing records 40-49 and have seen record 40 twice.Tortious
While interesting, a problem with this answer, as @Dissociation comment notes, is that you can only skip "forward" or "back" through results - you can't have "pages" you can jump to (e.g. Page 1, Page 2, Page 3) without making multiple sequential queries to work out where to start the pagination from, which I suspect going to be slower in most cases than just using skip. Of course you may not need to add the ability to skip to specific pages.Cocoon
This answer contains interesting points, but it doesn't answer the original question asked.Nitwit
lets get to a conclusion here. @Dissociation if skip is exactly right, then can it be added to this answer, or are you siding with Thomas's or Madhusudhan answer?Purity
@BrianThomas yes I would include .skip() in the answer. It is comparable to MySQL LIMIT 50,100, which is what the question is asking for.Dissociation
I have too personally experienced the poor performance of mongoDB when using skip. For initial pages it loads very fast, but while walking through the pages you can experience the slowness in retrieving data. We then used a similar approach as Chris mentioned. Since we didn't had a createdOn field, we applied the query on _id field. Since _id is already indexed the pagination was much faster.Papillon
@SarathNair won't that break if you use sorting?Halflength
Unless we have some sort of unique sequential ids, this approach will not work. Considering the above example, there can be multiple records with the same 'createdOn' date. How do we know if we included all the docs with the last 'createdOn' in the current page or some of them are pending? Someone mentioned the problem with non-sequential paging too. I think we are better-off with '.skip' unless there are millions of documents. If there are millions, we would need some filter on the results anyway.Peggi
This answer doesn't solve OP issue. This will not work as expected cause documents could have same createdOn, so they will be duplicated in the result and also there is no possibility to go to N page cause there is no information about request.createdOnBefore to start N page.Langbehn
Yes, timestamps will cause trouble if there is clustering on the same time. I recommend adding a sortable auto incrementable id to each record. It's fast (and it scales;)) and precise. But sometimes you can't use timestamps because you may sort by other criteria than time. If performance matters that much keep an external record counter document that you can refer to.Korean
How on earth is this paging?Archaean
Why has the author refused to answer any of the questions raised concerning his answer?Elspeth
I think in this case instead of using createdOn, maybe use increment id is better. createdOn maybe dupplicated.Whaleback
Just to add to the answer, it would be more efficient if the filter was on a field that is indexed. So instead of using createOn field, rather use _id for faster querying. This is possible only because _id by design has natural ordering. Reference: codementor.io/@arpitbhayani/…Jennine
I found this answer that gives a better example: https://mcmap.net/q/100734/-how-to-implement-pagination-for-mongodb-in-node-js-using-official-mongodb-clientSurely
Hmmmi.. Very interesting this answer open my mind. thank you. This is an interesting and beautiful way of pagination.Allemande
P
299

After taking a closer look at the Mongoose API with the information provided by Rodolphe, I figured out this solution:

MyModel.find(query, fields, { skip: 10, limit: 5 }, function(err, results) { ... });
Premises answered 4/4, 2011 at 15:15 Comment(10)
What about "count"? You need that to know how many pages there are.Baynebridge
@ChrisHinkle Why do you say that?Zwickau
Chris Hinkle's explanation why this does not scale: https://mcmap.net/q/98531/-how-to-paginate-with-mongoose-in-node-js .Orv
@ChrisHinkle This seems to be the case with all DBMSs. Lex's comment below the linked answer seems to explain more.Patriliny
This is problematic since you don't get the number of results and hence you can't make proper pagination.Aronow
If I query the collection by geoNear and sort by default distance, how can I avoid to use skip to do pagination?Magnesite
count of total results is required in most of cases to show the pages or total results to user.Carabineer
Answer is Ok. but this approach won't be so fast/efficient on db with large number of documents as it will first go through all of the documents before going to the skip point.Proterozoic
@GeorgeBailey I feel the same, collections having large number of records would have to be traversed a lot. Any other approach you could suggest?Ophidian
@Ophidian yeah. I've used identifier for that. what you do in there is send the last records id back to server and get some records with id greater than the sent. As Id is indexed so, it will be much fasterProterozoic
R
161

Pagination using mongoose, express and jade - Here's a link to my blog with more detail

var perPage = 10
  , page = Math.max(0, req.params.page)

Event.find()
    .select('name')
    .limit(perPage)
    .skip(perPage * page)
    .sort({
        name: 'asc'
    })
    .exec(function(err, events) {
        Event.count().exec(function(err, count) {
            res.render('events', {
                events: events,
                page: page,
                pages: count / perPage
            })
        })
    })
Rocketeer answered 11/2, 2013 at 22:29 Comment(5)
Thanks for posting your answer! Please be sure to read the FAQ on Self-Promotion carefully. Also note that it is required that you post a disclaimer every time you link to your own site/product.Cozy
Math.max(0, undefined) will return undefined , This worked for me: let limit = Math.abs(req.query.limit) || 10; let page = (Math.abs(req.query.page) || 1) - 1; Schema.find().limit(limit).skip(limit * page) Horal
be careful when receiving user input for page or limit, one of them could be ?limit=1e10000 which evaluates to infinity which causes Mongoose to throw a BadValue errorAfrit
.count is now deprecated. Use countDocuments() instead.Allanallana
You should consider setting your skip to 0 id its the first page.....add a check or somethingSybyl
S
70

You can chain just like that:

var query = Model.find().sort('mykey', 1).skip(2).limit(5)

Execute the query using exec

query.exec(callback);
Splenomegaly answered 4/4, 2011 at 14:40 Comment(3)
Thank you for your answer, how is the callback with the result added to this?Premises
execFind(function(... for example: var page = req.param('p'); var per_page = 10; if (page == null) { page = 0; } Location.count({}, function(err, count) { Location.find({}).skip(page*per_page).limit(per_page).execFind(function(err, locations) { res.render('index', { locations: locations }); }); });Colloquium
note: this will not work in mongoose, but it will work in mongodb-native-driver.Felly
M
53

In this case, you can add the query page and/ or limit to your URL as a query string.

For example:
?page=0&limit=25 // this would be added onto your URL: http:localhost:5000?page=0&limit=25

Since it would be a String we need to convert it to a Number for our calculations. Let's do it using the parseInt method and let's also provide some default values.

const pageOptions = {
    page: parseInt(req.query.page, 10) || 0,
    limit: parseInt(req.query.limit, 10) || 10
}

sexyModel.find()
    .skip(pageOptions.page * pageOptions.limit)
    .limit(pageOptions.limit)
    .exec(function (err, doc) {
        if(err) { res.status(500).json(err); return; };
        res.status(200).json(doc);
    });

BTW Pagination starts with 0

Mucin answered 7/6, 2016 at 2:19 Comment(12)
please add the `{ page: parseInt(req.query.page) || 0, ...} to the parameter.Raffle
@Raffle Thank you, however AFAIK string params are being handled automatically by mongoose.Mucin
Was expecting the behavior but in my case it couldn’t covert and showing me errorRaffle
@Raffle That's weird. Maybe if you could show a reproduction error I can edit my answer. Thanks.Mucin
so how would i know from the front end that there are results on page 2. I mean how can i tell when to show pagination or when to not,Zachar
@UsmanIqbal normally you'll just have to request for the next page, when the result is empty, then that's the end. Unless you explicitly tell current query that there are sill items on the next page. You can use metadata on the result such as isLastPage:false|trueMucin
hey man could you please give me or atleast show me the example. I am new to node and have very limited knowledgeZachar
@UsmanIqbal I suggest just add a "Load more" button, then just load the pages. If result is empty then that's the end. Hide the button. That is the easiest way I can suggest.Mucin
This is what I did ............................... var pageOptions={page:req.query.page||0,limit:10};Product.count(function(t,e){t?res.status(500).json(t):(productCount=e,totalPages=Math.ceil(e/pageOptions.limit),productCount>10?_meta={totalResults:productCount,currentPage:pageOptions.page,totalPages:totalPages,perPage:pageOptions.limit}:_meta={totalResults:productCount,perPage:pageOptions.limit})}),Product.find().skip(pageOptions.page*pageOptions.limit).limit(parseInt(pageOptions.limit)).exec("find",function(t,e){t?res.status(500).json(t):res.status(200).json({products:e,_meta:_meta})});Zachar
@UsmanIqbal can you provide a more readable code? Try Jsfiddle or codepenMucin
Would this cause mongoose to find every record before applying the conditions?Crocket
you need to parse the req.query.page and req.query.limit else it throws an error. use parseInt(req.query.limit) instead of req.query.limit directly etc.Carcanet
D
43

You can use a little package called Mongoose Paginate that makes it easier.

$ npm install mongoose-paginate

After in your routes or controller, just add :

/**
 * querying for `all` {} items in `MyModel`
 * paginating by second page, 10 items per page (10 results, page 2)
 **/

MyModel.paginate({}, 2, 10, function(error, pageCount, paginatedResults) {
  if (error) {
    console.error(error);
  } else {
    console.log('Pages:', pageCount);
    console.log(paginatedResults);
  }
}
Dystrophy answered 10/10, 2012 at 5:47 Comment(1)
Is this optimized ?Cuprite
T
37

Query:

search = productName

Params:

page = 1 

// Pagination
router.get("/search/:page", (req, res, next) => {
    const resultsPerPage = 5;
    let page = req.params.page >= 1 ? req.params.page : 1;
    const query = req.query.search;

    page = page - 1

    Product.find({ name: query })
        .select("name")
        .sort({ name: "asc" })
        .limit(resultsPerPage)
        .skip(resultsPerPage * page)
        .then((results) => {
            return res.status(200).send(results);
        })
        .catch((err) => {
            return res.status(500).send(err);
        });
});
Thad answered 21/4, 2020 at 23:9 Comment(4)
Thanks for this answer; I tried it first after reading through the thread because it was one of the more recent ones. However, when I implemented it, I discovered a bug - as it is written now, it will never return the first page of results, as it will ALWAYS have a skip value. Try adding "page = page-1" before the Product.find() call.Smriti
Thanks for your answer, But My self also found the bug that your page should be page = page - 1; I found that @interog also found that... so please edit your answer, so other people will easily go with your answer.Greerson
You missed the const declaration for page. It can't be reassignedWaltraudwaltz
Nice and clean answer!Kovacev
G
19

This is a example you can try this,

var _pageNumber = 2,
  _pageSize = 50;

Student.count({},function(err,count){
  Student.find({}, null, {
    sort: {
      Name: 1
    }
  }).skip(_pageNumber > 0 ? ((_pageNumber - 1) * _pageSize) : 0).limit(_pageSize).exec(function(err, docs) {
    if (err)
      res.json(err);
    else
      res.json({
        "TotalCount": count,
        "_Array": docs
      });
  });
 });
Gadgeteer answered 2/2, 2016 at 8:11 Comment(1)
Great answer! I was struggling with skipping logic.Australoid
S
13

Try using mongoose function for pagination. Limit is the number of records per page and number of the page.

var limit = parseInt(body.limit);
var skip = (parseInt(body.page)-1) * parseInt(limit);

 db.Rankings.find({})
            .sort('-id')
            .limit(limit)
            .skip(skip)
            .exec(function(err,wins){
 });
Suffolk answered 12/3, 2017 at 8:48 Comment(0)
S
10

This is what I done it on code

var paginate = 20;
var page = pageNumber;
MySchema.find({}).sort('mykey', 1).skip((pageNumber-1)*paginate).limit(paginate)
    .exec(function(err, result) {
        // Write some stuff here
    });

That is how I done it.

Salverform answered 21/10, 2014 at 8:0 Comment(4)
How to get the total no of pagesSensual
Hi @Rhushikesh, You can use a count() function to get the count. But it seems need to be another query from database. Details here mongoosejs.com/docs/api.html#model_Model.countSalverform
@Sensual get the count and divide it by the limitParonymous
count() is deprecated. use countDocuments()Yee
N
10

Simple and powerful pagination solution

async getNextDocs(no_of_docs_required: number = 5, last_doc_id?: string) {
    let docs

    if (!last_doc_id) {
        // get first 5 docs
        docs = await MySchema.find().sort({ _id: -1 }).limit(no_of_docs_required)
    }
    else {
        // get next 5 docs according to that last document id
        docs = await MySchema.find({_id: {$lt: last_doc_id}})
                                    .sort({ _id: -1 }).limit(no_of_docs_required)
    }
    return docs
}

last_doc_id: the last document id that you get

no_of_docs_required: the number of docs that you want to fetch i.e. 5, 10, 50 etc.

  1. If you don't provide the last_doc_id to the method, you'll get i.e. 5 latest docs
  2. If you've provided the last_doc_id then you'll get the next i.e. 5 documents.
Nuno answered 18/2, 2019 at 6:54 Comment(0)
L
8

There are some good answers giving the solution that uses skip() & limit(), however, in some scenarios, we also need documents count to generate pagination. Here's what we do in our projects:

const PaginatePlugin = (schema, options) => {
  options = options || {}
  schema.query.paginate = async function(params) {
    const pagination = {
      limit: options.limit || 10,
      page: 1,
      count: 0
    }
    pagination.limit = parseInt(params.limit) || pagination.limit
    const page = parseInt(params.page)
    pagination.page = page > 0 ? page : pagination.page
    const offset = (pagination.page - 1) * pagination.limit

    const [data, count] = await Promise.all([
      this.limit(pagination.limit).skip(offset),
      this.model.countDocuments(this.getQuery())
    ]);
    pagination.count = count;
    return { data, pagination }
  }
}

mySchema.plugin(PaginatePlugin, { limit: DEFAULT_LIMIT })

// using async/await
const { data, pagination } = await MyModel.find(...)
  .populate(...)
  .sort(...)
  .paginate({ page: 1, limit: 10 })

// or using Promise
MyModel.find(...).paginate(req.query)
  .then(({ data, pagination }) => {

  })
  .catch(err => {

  })
Linguiform answered 2/9, 2020 at 7:6 Comment(1)
Thanks @alvan. it works like a charm. For any one facing issue with 'property paginate does not exist in type' then use declare module 'mongoose' {interface DocumentQuery<T, DocType extends Document, QueryHelpers = {}> {paginate(options): any;}}Throughout
D
6

Here is a version that I attach to all my models. It depends on underscore for convenience and async for performance. The opts allows for field selection and sorting using the mongoose syntax.

var _ = require('underscore');
var async = require('async');

function findPaginated(filter, opts, cb) {
  var defaults = {skip : 0, limit : 10};
  opts = _.extend({}, defaults, opts);

  filter = _.extend({}, filter);

  var cntQry = this.find(filter);
  var qry = this.find(filter);

  if (opts.sort) {
    qry = qry.sort(opts.sort);
  }
  if (opts.fields) {
    qry = qry.select(opts.fields);
  }

  qry = qry.limit(opts.limit).skip(opts.skip);

  async.parallel(
    [
      function (cb) {
        cntQry.count(cb);
      },
      function (cb) {
        qry.exec(cb);
      }
    ],
    function (err, results) {
      if (err) return cb(err);
      var count = 0, ret = [];

      _.each(results, function (r) {
        if (typeof(r) == 'number') {
          count = r;
        } else if (typeof(r) != 'number') {
          ret = r;
        }
      });

      cb(null, {totalCount : count, results : ret});
    }
  );

  return qry;
}

Attach it to your model schema.

MySchema.statics.findPaginated = findPaginated;
Dagmar answered 14/2, 2013 at 15:50 Comment(0)
S
6

Above answer's holds good.

Just an add-on for anyone who is into async-await rather than promise !!

const findAllFoo = async (req, resp, next) => {
    const pageSize = 10;
    const currentPage = 1;

    try {
        const foos = await FooModel.find() // find all documents
            .skip(pageSize * (currentPage - 1)) // we will not retrieve all records, but will skip first 'n' records
            .limit(pageSize); // will limit/restrict the number of records to display

        const numberOfFoos = await FooModel.countDocuments(); // count the number of records for that model

        resp.setHeader('max-records', numberOfFoos);
        resp.status(200).json(foos);

    } catch (err) {
        resp.status(500).json({
            message: err
        });
    }
};
Swain answered 29/12, 2018 at 3:41 Comment(0)
R
5

you can use the following line of code as well

per_page = parseInt(req.query.per_page) || 10
page_no = parseInt(req.query.page_no) || 1
var pagination = {
  limit: per_page ,
  skip:per_page * (page_no - 1)
}
users = await User.find({<CONDITION>}).limit(pagination.limit).skip(pagination.skip).exec()

this code will work in latest version of mongo

Ribosome answered 27/2, 2019 at 8:30 Comment(0)
W
5

A solid approach to implement this would be to pass the values from the frontend using a query string. Let's say we want to get page #2 and also limit the output to 25 results.
The query string would look like this: ?page=2&limit=25 // this would be added onto your URL: http:localhost:5000?page=2&limit=25

Let's see the code:

// We would receive the values with req.query.<<valueName>>  => e.g. req.query.page
// Since it would be a String we need to convert it to a Number in order to do our
// necessary calculations. Let's do it using the parseInt() method and let's also provide some default values:

  const page = parseInt(req.query.page, 10) || 1; // getting the 'page' value
  const limit = parseInt(req.query.limit, 10) || 25; // getting the 'limit' value
  const startIndex = (page - 1) * limit; // this is how we would calculate the start index aka the SKIP value
  const endIndex = page * limit; // this is how we would calculate the end index

// We also need the 'total' and we can get it easily using the Mongoose built-in **countDocuments** method
  const total = await <<modelName>>.countDocuments();

// skip() will return a certain number of results after a certain number of documents.
// limit() is used to specify the maximum number of results to be returned.

// Let's assume that both are set (if that's not the case, the default value will be used for)

  query = query.skip(startIndex).limit(limit);

  // Executing the query
  const results = await query;

  // Pagination result 
 // Let's now prepare an object for the frontend
  const pagination = {};

// If the endIndex is smaller than the total number of documents, we have a next page
  if (endIndex < total) {
    pagination.next = {
      page: page + 1,
      limit
    };
  }

// If the startIndex is greater than 0, we have a previous page
  if (startIndex > 0) {
    pagination.prev = {
      page: page - 1,
      limit
    };
  }

 // Implementing some final touches and making a successful response (Express.js)

const advancedResults = {
    success: true,
    count: results.length,
    pagination,
    data: results
 }
// That's it. All we have to do now is send the `results` to the frontend.
 res.status(200).json(advancedResults);

I would suggest implementing this logic into middleware so you can be able to use it for various routes/ controllers.

Woodman answered 21/1, 2020 at 20:21 Comment(0)
D
4

You can do using mongoose-paginate-v2. For more info click here

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

const mySchema = new mongoose.Schema({
    // your schema code
}); 
mySchema.plugin(mongoosePaginate); 
const myModel = mongoose.model('SampleModel',  mySchema);

myModel.paginate().then({}) // Usage
Daffodil answered 16/7, 2020 at 10:57 Comment(0)
P
3

I have found a very efficient way and implemented it myself, I think this way is the best for the following reasons:

  • It does not use skip, which time complexity doesn't scale well;
  • It uses IDs to query the document. Ids are indexed by default in MongoDB, making them very fast to query;
  • It uses lean queries, these are known to be VERY performative, as they remove a lot of "magic" from Mongoose and returns a document that comes kind of "raw" from MongoDB;
  • It doesn't depend on any third party packages that might contain vulnerabilities or have vulnerable dependencies.

The only caveat to this is that some methods of Mongoose, such as .save() will not work well with lean queries, such methods are listed in this awesome blog post, I really recommend this series, because it considers a lot of aspects, such as type security (which prevents critical errors) and PUT/ PATCH.

I will provide some context, this is a Pokémon repository, the pagination works as the following: The API receives unsafeId from the req.body object of Express, we need to convert this to string in order to prevent NoSQL injections (it could be an object with evil filters), this unsafeId can be an empty string or the ID of the last item of the previous page, it goes like this:

 /**
   * @description GET All with pagination, will return 200 in success
   * and receives the last ID of the previous page or undefined for the first page
   * Note: You should take care, read and consider about Off-By-One error
   * @param {string|undefined|unknown} unsafeId - An entire page that comes after this ID will be returned
   */
  async readPages(unsafeId) {
    try {
      const id = String(unsafeId || '');
      let criteria;
      if (id) {
        criteria = {_id: {$gt: id}};
      } // else criteria is undefined

      // This query looks a bit redundant on `lean`, I just really wanted to make sure it is lean
      const pokemon = await PokemonSchema.find(
          criteria || {},
      ).setOptions({lean: true}).limit(15).lean();

      // This would throw on an empty page
      // if (pokemon.length < 1) {
      //  throw new PokemonNotFound();
      // }

      return pokemon;
    } catch (error) {
      // In this implementation, any error that is not defined by us
      // will not return on the API to prevent information disclosure.
      // our errors have this property, that indicate
      // that no sensitive information is contained within this object
      if (error.returnErrorResponse) {
        throw error;
      } // else
      console.error(error.message);
      throw new InternalServerError();
    }
  }

Now, to consume this and avoid Off-By-One errors in the frontend, you do it like the following, considering that pokemons is the Array of Pokémons documents that are returned from the API:

// Page zero
const pokemons = await fetchWithPagination({'page': undefined});
// Page one
// You can also use a fixed number of pages instead of `pokemons.length`
// But `pokemon.length` is more reliable (and a bit slower)
// You will have trouble with the last page if you use it with a constant
// predefined number 
const id = pokemons[pokemons.length - 1]._id;

if (!id) {
    throw new Error('Last element from page zero has no ID');
} // else

const page2 = await fetchWithPagination({'page': id});

As a note here, Mongoose IDs are always sequential, this means that any newer ID will always be greater than the older one, that is the foundation of this answer.

This approach has been tested agaisnt Off-By-One errors, for instance, the last element of a page could be returned as the first element of the following one (duplicated), or an element that is between the last of the previous page and the first of the current page might disappear.

When you are done with all the pages and request a page after the last element (one that does not exist), the response will be an empty array with 200 (OK), which is awesome!

Pietro answered 8/8, 2021 at 20:25 Comment(0)
C
3

There are many ways to implement it, but I will go with two

  1. find()
  2. aggregate()
const pageSize = 10;
const pageNumber = 1;

MyModel.find({})
  .sort({ createdAt: -1 })
  .skip(pageSize * (pageNumber - 1))
  .limit(pageSize)
  .exec((err, items) => {
    if (err) {
      // handle error
    }
    MyModel.countDocuments().exec((countError, count) => {
      if (countError) {
        // handle error
      }
      const totalPages = Math.ceil(count / pageSize);
      res.json({
        items,
        totalPages,
        currentPage: pageNumber,
      });
    });
  });

But here in find() function, I am hitting the database two times, but using aggregate(), we can do this in one query

const pageSize = 10;
const pageNumber = 1;

MyModel.aggregate([
  { $sort: { createdAt: -1 } },
  { $skip: pageSize * (pageNumber - 1) },
  { $limit: pageSize },
  { $group: { _id: null, count: { $sum: 1 }, items: { $push: "$$ROOT" } } },
]).exec((err, results) => {
  if (err) {
    // handle error
  }
  const { count, items } = results[0];
  const totalPages = Math.ceil(count / pageSize);
  res.json({
    items,
    totalPages,
    currentPage: pageNumber,
  });
});

Now, it is up to your requirement, which one you prefer. In most use cases, I prefer aggregate() instead of find() as it gives more tools to manipulate data.

Combined answered 15/3, 2023 at 18:2 Comment(0)
B
2

The easiest and more speedy way is, paginate with the objectId Example;

Initial load condition

condition = {limit:12, type:""};

Take the first and last ObjectId from response data

Page next condition

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c662d", lastId:"57762a4c875adce3c38c6615"};

Page next condition

condition = {limit:12, type:"next", firstId:"57762a4c875adce3c38c6645", lastId:"57762a4c875adce3c38c6675"};

In mongoose

var condition = {};
    var sort = { _id: 1 };
    if (req.body.type == "next") {
        condition._id = { $gt: req.body.lastId };
    } else if (req.body.type == "prev") {
        sort = { _id: -1 };
        condition._id = { $lt: req.body.firstId };
    }

var query = Model.find(condition, {}, { sort: sort }).limit(req.body.limit);

query.exec(function(err, properties) {
        return res.json({ "result": result);
});
Balkanize answered 8/7, 2016 at 11:8 Comment(0)
P
2

The best approach (IMO) is to use skip and limit BUT within a limited collections or documents.

To make the query within limited documents, we can use specific index like index on a DATE type field. See that below

let page = ctx.request.body.page || 1
let size = ctx.request.body.size || 10
let DATE_FROM = ctx.request.body.date_from
let DATE_TO = ctx.request.body.date_to

var start = (parseInt(page) - 1) * parseInt(size)

let result = await Model.find({ created_at: { $lte: DATE_FROM, $gte: DATE_TO } })
    .sort({ _id: -1 })
    .select('<fields>')
    .skip( start )
    .limit( size )        
    .exec(callback)
Plated answered 8/9, 2017 at 0:44 Comment(0)
R
2

Most easiest plugin for pagination.

https://www.npmjs.com/package/mongoose-paginate-v2

Add plugin to a schema and then use model paginate method:

var mongoose         = require('mongoose');
var mongoosePaginate = require('mongoose-paginate-v2');

var mySchema = new mongoose.Schema({ 
    /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

var myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage
Rn answered 18/7, 2018 at 9:37 Comment(1)
this plugin is broken with mongoose v5.5.5Pluvial
P
2
let page,limit,skip,lastPage, query;
 page = req.params.page *1 || 1;  //This is the page,fetch from the server
 limit = req.params.limit * 1 || 1; //  This is the limit ,it also fetch from the server
 skip = (page - 1) * limit;   // Number of skip document
 lastPage = page * limit;   //last index 
 counts = await userModel.countDocuments() //Number of document in the collection

query = query.skip(skip).limit(limit) //current page

const paginate = {}

//For previous page
if(skip > 0) {
   paginate.prev = {
       page: page - 1,
       limit: limit
} 
//For next page
 if(lastPage < counts) {
  paginate.next = {
     page: page + 1,
     limit: limit
}
results = await query //Here is the final results of the query.
Polished answered 30/3, 2020 at 17:13 Comment(0)
S
1

This is example function for getting the result of skills model with pagination and limit options

 export function get_skills(req, res){
     console.log('get_skills');
     var page = req.body.page; // 1 or 2
     var size = req.body.size; // 5 or 10 per page
     var query = {};
     if(page < 0 || page === 0)
     {
        result = {'status': 401,'message':'invalid page number,should start with 1'};
        return res.json(result);
     }
     query.skip = size * (page - 1)
     query.limit = size
     Skills.count({},function(err1,tot_count){ //to get the total count of skills
      if(err1)
      {
         res.json({
            status: 401,
            message:'something went wrong!',
            err: err,
         })
      }
      else 
      {
         Skills.find({},{},query).sort({'name':1}).exec(function(err,skill_doc){
             if(!err)
             {
                 res.json({
                     status: 200,
                     message:'Skills list',
                     data: data,
                     tot_count: tot_count,
                 })
             }
             else
             {
                 res.json({
                      status: 401,
                      message: 'something went wrong',
                      err: err
                 })
             }
        }) //Skills.find end
    }
 });//Skills.count end

}

Superconductivity answered 19/9, 2018 at 12:33 Comment(0)
D
1

Using ts-mongoose-pagination

    const trainers = await Trainer.paginate(
        { user: req.userId },
        {
            perPage: 3,
            page: 1,
            select: '-password, -createdAt -updatedAt -__v',
            sort: { createdAt: -1 },
        }
    )

    return res.status(200).json(trainers)
Doradorado answered 29/3, 2020 at 3:21 Comment(0)
B
1

The MongoDB official blog has an entry about pagination, where they go through why 'skip' may be slow and offer alternatives: https://www.mongodb.com/blog/post/paging-with-the-bucket-pattern--part-1

Brewing answered 28/6, 2021 at 21:43 Comment(0)
A
1

Below Code Is Working Fine For Me. You can add finding filters also and user same in countDocs query to get accurate results.

export const yourController = async (req, res) => {
  const { body } = req;

  var perPage = body.limit,
  var page = Math.max(0, body.page);

  yourModel
    .find() // You Can Add Your Filters inside
    .limit(perPage)
    .skip(perPage * (page - 1))
    .exec(function (err, dbRes) {
      yourModel.count().exec(function (err, count) { // You Can Add Your Filters inside
        res.send(
          JSON.stringify({
            Articles: dbRes,
            page: page,
            pages: count / perPage,
          })
        );
      });
    });
};
Amalekite answered 23/6, 2022 at 11:54 Comment(0)
F
1
const page = req.query.page * 1 || 1;
const limit = req.query.limit * 1 || 1000;
const skip = (page - 1) * limit;

query = query.skip(skip).limit(limit);
Fbi answered 27/10, 2022 at 5:2 Comment(1)
While this code may answer the question, providing additional context regarding how and/or why it solves the problem would improve the answer's long-term value. You can find more information on how to write good answers in the help center: stackoverflow.com/help/how-to-answer . Good luckBrosine
P
0

You can write query like this.

mySchema.find().skip((page-1)*per_page).limit(per_page).exec(function(err, articles) {
        if (err) {
            return res.status(400).send({
                message: err
            });
        } else {
            res.json(articles);
        }
    });

page : page number coming from client as request parameters.
per_page : no of results shown per page

If you are using MEAN stack following blog post provides much of the information to create pagination in front end using angular-UI bootstrap and using mongoose skip and limit methods in the backend.

see : https://techpituwa.wordpress.com/2015/06/06/mean-js-pagination-with-angular-ui-bootstrap/

Poignant answered 6/6, 2015 at 17:7 Comment(0)
D
0

You can either use skip() and limit(), but it's very inefficient. A better solution would be a sort on indexed field plus limit(). We at Wunderflats have published a small lib here: https://github.com/wunderflats/goosepage It uses the first way.

Dunite answered 5/12, 2015 at 19:27 Comment(0)
M
0

If you are using mongoose as a source for a restful api have a look at 'restify-mongoose' and its queries. It has exactly this functionality built in.

Any query on a collection provides headers that are helpful here

test-01:~$ curl -s -D - localhost:3330/data?sort=-created -o /dev/null
HTTP/1.1 200 OK
link: </data?sort=-created&p=0>; rel="first", </data?sort=-created&p=1>; rel="next", </data?sort=-created&p=134715>; rel="last"
.....
Response-Time: 37

So basically you get a generic server with a relatively linear load time for queries to collections. That is awesome and something to look at if you want to go into a own implementation.

Margarettmargaretta answered 8/3, 2016 at 10:53 Comment(0)
B
0
app.get("/:page",(req,res)=>{
        post.find({}).then((data)=>{
            let per_page = 5;
            let num_page = Number(req.params.page);
            let max_pages = Math.ceil(data.length/per_page);
            if(num_page == 0 || num_page > max_pages){
                res.render('404');
            }else{
                let starting = per_page*(num_page-1)
                let ending = per_page+starting
                res.render('posts', {posts:data.slice(starting,ending), pages: max_pages, current_page: num_page});
            }
        });
});
Binomial answered 14/7, 2017 at 16:16 Comment(0)
R
0
**//localhost:3000/asanas/?pageNo=1&size=3**

//requiring asanas model
const asanas = require("../models/asanas");


const fetchAllAsanasDao = () => {
    return new Promise((resolve, reject) => {

    var pageNo = parseInt(req.query.pageNo);
    var size = parseInt(req.query.size);
    var query = {};
        if (pageNo < 0 || pageNo === 0) {
            response = {
                "error": true,
                "message": "invalid page number, should start with 1"
            };
            return res.json(response);
        }
        query.skip = size * (pageNo - 1);
        query.limit = size;

  asanas
            .find(pageNo , size , query)
        .then((asanasResult) => {
                resolve(asanasResult);
            })
            .catch((error) => {
                reject(error);
            });

    });
}
Renfrew answered 6/7, 2018 at 9:7 Comment(0)
S
0

Use this simple plugin.

https://github.com/WebGangster/mongoose-paginate-v2

Installation

npm install mongoose-paginate-v2
Usage Add plugin to a schema and then use model paginate method:

const mongoose         = require('mongoose');
const mongoosePaginate = require('mongoose-paginate-v2');

const mySchema = new mongoose.Schema({ 
  /* your schema definition */ 
});

mySchema.plugin(mongoosePaginate);

const myModel = mongoose.model('SampleModel',  mySchema); 

myModel.paginate().then({}) // Usage
Shouse answered 18/9, 2019 at 15:49 Comment(2)
This plugin has been "suggested" already in another answer. It would also be helpful to know that you if are a contributor on this package.Olodort
@Olodort Yes. I'm the creator of the plugin.Shouse
L
0
const ITEMS_PER_PAGE = 2;

exports.getProducts = (req, res, next) => {
  // + will turn the string to a number
  const page = +req.query.page || 1;
  let totalItems;
  //Product model
  Product.find()
    .countDocuments()
    .then((numProducts) => {
      totalItems = numProducts;
      return Product.find()
         //If query param is 3, since ITEMS_PER_PAGE = 2, we skip 2*2 items   
         // we show only 5th and 6th item
        .skip((page - 1) * ITEMS_PER_PAGE)
        .limit(ITEMS_PER_PAGE);
    })
    .then((products) => {
      res.render("shop/products", {
        // maybe sending the products object to templating engine
      });
    })
    .catch((err) => {
      const error = new Error(err);
      error.httpStatusCode = 500;
      // if you are set express error handler, use this
      // when we call next() with an argument passed in, we let express know, we skip all other middlewares, we move to error handling middleware

      return next(error);
    });
};
Liggett answered 26/3, 2022 at 3:29 Comment(0)
L
-1

Was able to achieve results with async/await as well.

Code example below using an async handler with hapi v17 and mongoose v5

{
            method: 'GET',
            path: '/api/v1/paintings',
            config: {
                description: 'Get all the paintings',
                tags: ['api', 'v1', 'all paintings']
            },
            handler: async (request, reply) => {
                /*
                 * Grab the querystring parameters
                 * page and limit to handle our pagination
                */
                var pageOptions = {
                    page: parseInt(request.query.page) - 1 || 0, 
                    limit: parseInt(request.query.limit) || 10
                }
                /*
                 * Apply our sort and limit
                */
               try {
                    return await Painting.find()
                        .sort({dateCreated: 1, dateModified: -1})
                        .skip(pageOptions.page * pageOptions.limit)
                        .limit(pageOptions.limit)
                        .exec();
               } catch(err) {
                   return err;
               }

            }
        }
Lascar answered 18/1, 2019 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.