Skip to Content
Lesson 5

Advanced Mongoose Querying

Workplace Context

Your application’s “view all products” page is becoming slow and returning too much data. Users are complaining that they can’t filter by price, sort by popularity, or navigate through pages of results. Your task is to optimize the API endpoint by implementing more advanced Mongoose queries. You need to filter results using operators, select only the necessary data fields, sort the results, and add pagination to handle the large dataset efficiently.


Learning Objectives

By the end of this lesson, you will be able to:

  • Filter queries using comparison operators like $gt (greater than) and $in (in array).
  • Control which fields are returned from a query using projections.
  • Sort query results in ascending or descending order.
  • Implement pagination using the .limit() and .skip() query methods.

Query and Comparison Operators

So far, you’ve queried for documents by matching exact values (e.g., { author: 'John Doe' }). But what if you want to find all products with a price greater than $50? For this, you need comparison operators.

In MongoDB (and Mongoose), these operators start with a dollar sign ($).

Here are some of the most common operators:

OperatorNameDescription
$eq”equal to”Matches values that are equal.
$ne”not equal to”Matches values that are not equal.
$gt”greater than”Matches values greater than the value.
$gte”greater than or equal to”Matches values greater than or equal to the value.
$lt”less than”Matches values less than the value.
$lte”less than or equal to”Matches values less than or equal to the value.
$in”in”Matches any of the values in an array.
$nin”not in”Matches none of the values in an array.

Example: Using Operators

Let’s assume we have a Product model. Here’s how to use these operators:

// Find all products with a price greater than or equal to 100 Product.find({ price: { $gte: 100 } }); // Find all products that are in the 'Electronics' or 'Books' category Product.find({ category: { $in: ['Electronics', 'Books'] } }); // Find all products that are NOT in the 'Clothing' category Product.find({ category: { $ne: 'Clothing' } });

Projections: Selecting Specific Fields

By default, Mongoose queries return all fields in the matching documents. This can be inefficient if you only need a few fields. For example, on a product list page, you might only need the name, price, and thumbnail image, not the full description and list of reviews.

Projections let you specify which fields to include or exclude from the result. You provide a projection object as the second argument to find() or findOne().

  • Use a 1 or true to include a field.
  • Use a 0 or false to exclude a field.
Note

You cannot mix inclusion and exclusion in the same projection, with one exception: you can always explicitly exclude the _id field. By default, _id is always included.

Example: Using Projections

// Only include the name and price of all products // The _id will be included by default Product.find({}, { name: 1, price: 1 }) .then(products => { // Each `product` object will ONLY have name, price, and _id fields console.log(products); }); // Exclude the lengthy description and reviews // The `_id` field is still included Product.find({}, { description: 0, reviews: 0 }); // Include name and price, but explicitly exclude the _id Product.find({}, { name: 1, price: 1, _id: 0 });

Sorting

You can sort the results of a query using the .sort() method. You pass an object where the keys are the fields to sort by and the values are either 1 (for ascending order) or -1 (for descending order).

Example: Sorting Queries

// Find all products and sort them by price, from lowest to highest (ascending) Product.find({}).sort({ price: 1 }); // Find all products and sort them by creation date, from newest to oldest (descending) Product.find({}).sort({ createdAt: -1 }); // You can also sort by multiple fields. It will sort by the first, // then by the second for any documents where the first field is the same. // Sort by category alphabetically, then by price from highest to lowest. Product.find({}).sort({ category: 1, price: -1 });

Pagination

If you have thousands of documents, you don’t want to send them all to the client at once. Pagination is the process of breaking up a large result set into smaller, manageable “pages.”

Mongoose makes this easy with two methods:

  • .limit(number): Specifies the maximum number of documents the query will return (the “page size”).
  • .skip(number): Specifies the number of documents to skip from the beginning of the result set.

By combining skip and limit, you can create a simple pagination system.

Example: Implementing Pagination

Let’s say we want to display 10 products per page.

const page = 3; // The page number we want to retrieve const pageSize = 10; // The number of items per page // Page 1: skip(0).limit(10) -> returns documents 1-10 // Page 2: skip(10).limit(10) -> returns documents 11-20 // Page 3: skip(20).limit(10) -> returns documents 21-30 Product.find({}) .sort({ name: 1 }) .skip((page - 1) * pageSize) .limit(pageSize) .then(paginatedResults => { console.log(`Showing page ${page} of results:`, paginatedResults); });

These methods are chainable and are typically applied at the end of a query, right before you execute it.


Chaining It All Together

The true power of Mongoose’s query API comes from its chainability. You can combine all the methods we’ve discussed — filtering, projecting, sorting, and paginating — into a single, elegant, and highly readable query chain. The order in which you chain these methods is flexible, as Mongoose builds the full query object before sending it to MongoDB.

Example: A Complex API Query

Imagine you’re building an API endpoint for an e-commerce site that needs to:

  1. Find all products in the ‘Electronics’ category.
  2. That cost more than $100.
  3. Return only their name, price, and stock fields.
  4. Sort them by price from highest to lowest.
  5. Return the first 5 products for the first page of results.

Here is how you would construct this query:

const page = 1; const limit = 5; Product.find({ category: 'Electronics', price: { $gt: 100 } }) .select({ name: 1, price: 1, stock: 1, _id: 0 }) // .select() is an alias for projection .sort({ price: -1 }) .skip((page - 1) * limit) .limit(limit) .then(results => { console.log('Final paginated, sorted, and projected results:', results); }) .catch(err => { console.error('Complex query failed:', err); });
Note

.select() is a dedicated method for projections that can sometimes lead to cleaner code than passing the projection object as the second argument to .find(). It does the exact same thing.

This single, chained command is highly efficient. Instead of fetching all data and then processing it in your Node.js application (filtering, sorting), you are telling the database to do all the heavy lifting. The database is highly optimized for these operations, which means your application uses less memory and your API responds much faster.


Activities

Activity 1: Filter, Project, and Sort

  1. Using a Product model, write a single Mongoose query that does all of the following:
    • Finds all products where the price is less than 50.
    • Only returns the name and price fields.
    • Sorts the results by price in descending order (highest to lowest).
  2. Log the results to the console.

Activity 2: Build a Paginated API Endpoint

  1. Create an Express GET /api/products endpoint that supports pagination.
  2. The endpoint should accept two optional query parameters: page (defaults to 1) and limit (defaults to 10).
  3. Use these parameters with the .skip() and .limit() methods to fetch the correct “page” of products from your database.
  4. Return the array of products as a JSON response.
  5. Test your endpoint with various page and limit values (e.g., /api/products?page=2&limit=5).

Knowledge Check

How would you write a query to find all users whose age is between 25 and 35 (inclusive)?

  • Select an answer to view feedback.

What is the primary purpose of using a projection in a Mongoose query?

  • Select an answer to view feedback.

To get the third page of results in a list where each page has 20 items, what would your .skip() and .limit() methods look like?

  • Select an answer to view feedback.

Summary

In this lesson, you moved beyond basic queries and learned how to build powerful, efficient, and professional-grade database interactions. You can now filter results with comparison operators, reduce data transfer with projections, order results with sorting, and handle large datasets gracefully with pagination. These four techniques are essential for building scalable and performant APIs.


References