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:
Operator | Name | Description |
---|---|---|
$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
ortrue
to include a field. - Use a
0
orfalse
to exclude a field.
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:
- Find all products in the ‘Electronics’ category.
- That cost more than $100.
- Return only their
name
,price
, andstock
fields. - Sort them by price from highest to lowest.
- 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);
});
.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
- 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
andprice
fields. - Sorts the results by
price
in descending order (highest to lowest).
- Finds all products where the
- Log the results to the console.
Activity 2: Build a Paginated API Endpoint
- Create an Express
GET /api/products
endpoint that supports pagination. - The endpoint should accept two optional query parameters:
page
(defaults to 1) andlimit
(defaults to 10). - Use these parameters with the
.skip()
and.limit()
methods to fetch the correct “page” of products from your database. - Return the array of products as a JSON response.
- Test your endpoint with various
page
andlimit
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.