Implementing Query Filtering in Express with Mongoose
In modern API development, providing flexible querying mechanisms is essential to allow clients to filter and retrieve data efficiently. In this post, we’ll go over how to implement query filtering using Express.js and Mongoose, focusing on handling MongoDB operators like $gt, $gte, $lt, $lte, and $in.
Problem Overview
We want to create a route that returns bootcamp records from the database based on query parameters, allowing users to filter results using specific MongoDB operators.
Initial Code
Let’s start by reviewing an example of a simple controller function for fetching bootcamps:
exports.getBootcamps = asyncHandler(async (req, res, next) => {
let query;
let queryStr = JSON.stringify(req.query);
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);
query = Bootcamp.find(JSON.parse(queryStr));
const bootcamps = await Bootcamp.find();
res
.status(200)
.json({ success: true, count: bootcamps.length, data: bootcamps });
});
At first glance, this code looks good. It is preparing a MongoDB query based on the request’s query parameters. However, there’s a critical issue: the actual query (query) is not being used when fetching the bootcamps. Instead, Bootcamp.find() is called again, which retrieves all the bootcamps without any filtering.
The Problem
In the above code, the query logic using Bootcamp.find(JSON.parse(queryStr)) is correctly set up, but it’s never executed. Instead, the second query Bootcamp.find() is called without any filters. This essentially bypasses all the filtering logic we’ve created.
The Fix
To resolve this, we need to make sure that the properly filtered query (query) is executed when fetching the bootcamps. Let’s update the function to address this:
exports.getBootcamps = asyncHandler(async (req, res, next) => {
let query;
// Copy req.query
let queryStr = JSON.stringify(req.query);
// Replace MongoDB operators like gt, gte, lt, lte, in with $ prefix
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match => `$${match}`);
// Parse the modified query string into an object
query = Bootcamp.find(JSON.parse(queryStr));
// Execute the query
const bootcamps = await query;
// Send the filtered data back as the response
res
.status(200)
.json({
success: true,
count: bootcamps.length,
data: bootcamps
});
});
Explanation of the Fix
- Copying the Query String:
let queryStr = JSON.stringify(req.query);This line copies the query parameters from the request into a string that we can modify.
- Modifying the Query:
queryStr = queryStr.replace(/\b(gt|gte|lt|lte|in)\b/g, match =>$${match});This line replaces MongoDB operator keywords (likegt,gte,lt,lte, andin) with their corresponding MongoDB query format, prepending$. This allows us to translate the client’s request into a format that MongoDB understands.
- Parsing the Query String:
query = Bootcamp.find(JSON.parse(queryStr));After modifying the query string, it is converted back into an object and passed to Mongoose’sfind()function. This prepares the database query with the correct filters.
- Executing the Query:
const bootcamps = await query;This executes the prepared query to fetch the filtered data from the database.
- Sending the Response:
- The response now contains the filtered data, returning the result count and the data itself.
Testing the Filtered Query
Let’s test this implementation by sending a few example API requests:
Filter by Price Greater Than a Value:
GET /api/v1/bootcamps?price[gt]=1000
This query will return bootcamps with prices greater than 1000.
Filter by Rating Between Values:
GET /api/v1/bootcamps?rating[gte]=4&rating[lte]=5
This query will return bootcamps with ratings between 4 and 5.
Filter by Multiple IDs:
GET /api/v1/bootcamps?id[in]=5f08e7c8ad1b1e22d84674dd,5f08e7c8ad1b1e22d84674de
- This query will return bootcamps whose IDs match any of the provided values.
Conclusion
By modifying the query string to accommodate MongoDB operators and ensuring the filtered query is executed, we’ve created a robust query filtering mechanism for our API. This approach allows clients to dynamically filter the data based on various conditions like greater than ($gt), less than ($lt), and many others.
This is a crucial feature in API development, providing flexibility for end-users to retrieve data efficiently without needing multiple endpoints for different query types.