Every Mongoose query, one page

Cheatsheet 8 min read Updated August 2026

The Mongoose queries you reach for every day, in one place. Bookmark it — find, filter, sort, paginate, update, and populate, plus the lean() tip that makes reads fast.

Finding documents

await User.find()                         // all documents
await User.find({ active: true })         // filter
await User.findOne({ email })             // first match
await User.findById(id)                   // by _id
await User.countDocuments({ active: true })
await User.exists({ email })              // truthy if any match

Query operators

// comparison
{ age: { $gt: 18, $lte: 65 } }            // > and <=
{ role: { $in: ['admin', 'staff'] } }     // in a set
{ score: { $ne: 0 } }                     // not equal

// logical
{ $or: [{ a: 1 }, { b: 2 }] }
{ $and: [{ active: true }, { age: { $gte: 18 } }] }

// element / text
{ name: { $regex: /^jo/i } }              // pattern
{ tags: { $exists: true } }               // field present

Projection & sorting

await User.find(filter, 'name email')     // include fields
await User.find(filter).select('-password') // exclude
await User.find(filter).sort({ createdAt: -1 }) // desc
await User.find(filter).sort('name -age') // asc name, desc age
Practice this
Write real Mongoose queries
Build MERN features against MongoDB in a full IDE — with automated grading.
Practice MERN

Pagination

// offset-based (simple)
const page = 2, size = 20
await User.find(filter)
  .skip((page - 1) * size)
  .limit(size)

// cursor-based (fast for large sets, needs an index)
await User.find({ _id: { $gt: lastId } })
  .sort({ _id: 1 })
  .limit(size)

Creating & updating

await User.create({ name, email })        // insert one
await User.insertMany(docs)               // bulk insert

await User.updateOne({ _id: id }, { $set: { name } })
await User.updateMany({ active: false }, { $set: { active: true } })

// return the updated doc
await User.findByIdAndUpdate(id, { $inc: { visits: 1 } },
  { new: true })

// create if missing
await User.updateOne({ email }, { $set: { name } },
  { upsert: true })

// array updates
{ $push: { tags: 'new' } }                // add
{ $pull: { tags: 'old' } }                // remove
{ $addToSet: { tags: 'unique' } }         // add if absent

Deleting

await User.deleteOne({ _id: id })
await User.deleteMany({ active: false })
await User.findByIdAndDelete(id)          // returns the doc

Populate (references)

await Order.find().populate('user')       // resolve ref
await Order.find().populate('user', 'name email') // pick fields
await Order.find().populate({
  path: 'items',
  match: { inStock: true },
  options: { sort: { price: 1 } },
})

Performance

await User.find(filter).lean()            // plain objects, faster reads
await User.find(filter).select('name')    // fetch only what you need
await User.find(filter).sort(...).explain() // check index usage

Use lean() for read-only queries, project only the fields you need, and make sure filters and sorts are backed by an index.

FAQ

What does lean() do in Mongoose?

lean() tells Mongoose to return plain JavaScript objects instead of full Mongoose documents. It skips hydration, getters, and change tracking, so reads are noticeably faster — use it whenever you only need to read data, not modify and save it.

How do I paginate results in Mongoose?

For simple cases use .skip() and .limit(); skip((page-1)*size).limit(size). For large or frequently changing collections, prefer cursor-based pagination on an indexed field (e.g. _id greater-than the last seen value) to avoid slow deep skips.

When should I use populate()?

Use populate() to resolve referenced documents from another collection into your results. It's convenient, but each populate is an extra query — for hot paths consider the extended-reference pattern (storing the few fields you need) instead.

Put it into practice

Solve real challenges in a full browser IDE — graded automatically.

Browse frameworks