
Join the Conversation!
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
By now you should have a solid understanding of how to use Mongoose to interact with MongoDB and make some powerful queries.
In this lesson, we're going to learn how to paginate data. Pagination is a common technique used to break up large datasets into smaller, more manageable chunks.
We'll be using this pattern in our application to display a list of items in the search page.
Take a look at the page .
You'll notice we have filters to the left- to sort our products by:
And to the right, we have some placeholder product display components.
The application is already setup to push whatever we select in the filters to our URL, so our main goal will be integrating this logic with our backend and database.
Let's begin by simply writing out what information we need from our results for the search page.
We need:
And each product should have:
We should be able to sort the products by:
That's a lot! Let's first start by making sure we can fetch the products from our database, in the shape of the data we need. Our first problem is that each product doesn't have a rating in our schema. So we can't directly fetch the rating for each product just by querying the products collection.
We'll also need reviews!
Let's leverage the power of Mongoose and aggregates to fetch all the reviews for each product.
products.ts;
export async function getProducts() {
await dbConnect();
try {
const products = await Product.aggregate([
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "product",
as: "reviews",
},
},
]);
return products;
} catch (error) {
console.log(error);
}
}
Now our results will include the reviews, which we can use to calculate the average rating for each product.
//
{
"_id": "123",
"name": "Product 1",
"price": 100,
"category": "Category 1",
"reviews": [
{
"_id": "456",
"author": {
"name": "John Doe",
"email": "john@email.com"
},
"content": "Great product!",
"rating": 5
},
{
"_id": "789",
"author": {
"name": "Jane Doe",
"email": "jane@email.com"
},
"content": "Not bad",
"rating": 4
}
]
}
Let's use to calculate the average rating for each product.
products.ts;
export async function getProducts() {
await dbConnect();
try {
const products = await Product.aggregate([
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "product",
as: "reviews",
},
},
{
$project: {
name: 1,
images: 1,
averageRating: {
$avg: "$reviews.rating",
},
},
},
]);
return products;
} catch (error) {
console.log(error);
}
}
Now our results will include the average rating for each product.
//
{
"_id": "123",
"name": "Product 1",
"category": "Category 1",
"averageRating": 4.5
}
Remember, we're using to fetch the products and reviews. This is a powerful tool that allows us to perform complex queries in order like a pipeline to transform our data.
So first, we added the reviews in one step, and then we calculated the average rating in another step.
We've almost made our first step! We can now fetch the products with their average rating. But we still want our data to just contain a single image, instead of an array of images.
Let's use inside our $project to get the first image from the array of images.
products.ts;
export async function getProducts() {
await dbConnect();
try {
const products = await Product.aggregate([
{
$lookup: {
from: "reviews",
localField: "_id",
foreignField: "product",
as: "reviews",
},
},
{
$project: {
name: 1,
// just grab the first element
image: { $first: "$images" },
averageRating: {
$avg: "$reviews.rating",
},
},
},
]);
return products;
} catch (error) {
console.log(error);
}
}
Now our data should be shaped just how we want.
//
{
"_id": "123",
"name": "Product 1",
"image": "https://example.com/image.jpg",
"averageRating": 4.5
}
Next, let's add pagination to our query. We'll add a to our query to fetch a specific number of products for each page.
The first step is to understand what both of those commands will do in our aggregate.
If we want to just grab the first 5 products, we would use with a value of 5, and we would use with a value of 0.
Here's what those two stages in our aggregate would look like:
//
const products = await Product.aggregate([
{
$lookup: {
// ...
},
},
{
$project: {
// ...
},
},
{
$skip: 0,
},
{
$limit: 5,
},
]);
As you can see, we're simply adding 2 more stages to our pipeline.
Now let's decide how many results to show per page. We'll add a of 5 to our query.
products.ts
export async function getProducts(page: number){
await dbConnect();
// hardcode a limit of 5
const limit = 5;
try{
const products = await Product.aggregate([
{
$lookup: {
from: 'reviews',
localField: '_id',
foreignField: 'product',
as: 'reviews'
}
},
{
$project: {
name: 1,
image: { $first: '$images' },
averageRating: {
$avg: '$reviews.rating'
}
}
},
{
$skip: 0
},
{
$limit: limit
},
]);
return products;
} catch (error){
console.log(error);
}
}
Next we'll do a calulation- given a certain page number, how many results should we skip?
If we're on page 1, we should skip 0 results. If we're on page 2, we should skip 5 results. If we're on page 3, we should skip 10 results.
We can calculate this by subtracting 1 from the page number and multiplying it by the limit.
This will be our skip value.
We'll add an argument for the page number, and implement the skipping logic.
products.ts
export async function getProducts(page: number){
await dbConnect();
const limit = 5;
const skip = (page - 1) * limit;
try{
const products = await Product.aggregate([
{
$lookup: {
from: 'reviews',
localField: '_id',
foreignField: 'product',
as: 'reviews'
}
},
{
$project: {
name: 1,
image: { $first: '$images' },
averageRating: {
$avg: '$reviews.rating'
}
}
},
{
$skip: skip
},
{
$limit: limit
},
]);
return products;
} catch (error){
console.log(error);
}
}
Nice! Now we can fetch a specific number of products for each page.
Next, let's add the ability to filter our products by name, price, and category.
Let's start with name.
We'll be passing in a search term to our function, and we'll use to filter our products by name.
products.ts
export async function getProducts(page: number, search: string){
await dbConnect();
const limit = 5;
const skip = (page - 1) * limit;
try{
const products = await Product.aggregate([
{
$lookup: {
// ...
}
},
{
$project: {
// ...
}
},
{
$match: {
name: {
$regex: search,
$options: 'i'
}
}
},
{
$skip: skip
},
{
$limit: limit
},
]);
return products;
} catch (error){
console.log(error);
}
}
We're using to perform a case-insensitive search for the name field.
The flag 'i' makes the search case-insensitive.
Now if we pass in a string to our function, we'll only get products who's name are somewhere in that string.
If we pass an empty string, we'll get all products.
Next, let's add the ability to filter by minimum price.
We'll add a argument to our function, and use to filter our products by price.
products.ts
export async function getProducts(page: number, search: string, minPrice: number){
await dbConnect();
const limit = 5;
const skip = (page - 1) * limit;
try{
const products = await Product.aggregate([
{
$lookup: {
// ...
}
},
{
$project: {
// ...
}
},
{
$match: {
name: {
$regex: search,
$options: 'i'
},
price: {
$gte: minPrice
}
}
},
{
$skip: skip
},
{
$limit: limit
},
]);
return products;
} catch (error){
console.log(error);
}
}
We're using to filter products with a price greater than or equal to the minPrice.
Now if we pass in a minPrice, we'll only get products with a price greater than or equal to that value.
If we pass in 0, we'll get all products.
Next, let's add the ability to filter by category.
We'll add a argument to our function, and use to filter our products by category.
products.ts
export async function getProducts(page: number, search: string, minPrice: number, category: string){
await dbConnect();
const limit = 5;
const skip = (page - 1) * limit;
try{
const products = await Product.aggregate([
{
$lookup: {
// ...
}
},
{
$project: {
// ...
}
},
{
$match: {
name: {
$regex: search,
$options: 'i'
},
price: {
$gte: minPrice
},
category: category
}
},
{
$skip: skip
},
{
$limit: limit
},
]);
return products;
} catch (error){
console.log(error);
}
}
Now if we pass in a category, we'll only get products with that category.
Uh oh! We have a problem. If we pass in an empty string for the category, we won't get any results.
Let's use conditional logic to only add the category filter if a category is passed in.
products.ts
export async function getProducts(page: number, search: string, minPrice: number, category: string){
await dbConnect();
const limit = 5;
const skip = (page - 1) * limit;
try{
const pipeline = [
{
$lookup: {
// ...
}
},
{
$project: {
// ...
}
},
{
$match: {
name: {
$regex: search,
$options: 'i'
},
price: {
$gte: minPrice
}
...(category && { category: category })
}
},
{
$skip: skip
},
{
$limit: limit
},
];
const products = await Product.aggregate(pipeline);
return products;
} catch (error){
console.log(error);
}
}
Now if we pass in an empty string for the category, we'll get all products, because we simply won't add the category filter to our pipeline.
We've successfully implemented pagination and filtering in our application, now all we have to do is connect our frontend to our backend!
In the next lesson, we'll integrate this complex query into our search page, and display the results in our product display components.
See you there!
"Please login to view comments"
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.