W05 Learning Activity: Searching and Filtering
Overview
- Purpose: Learn about searching and filtering.
- Task: Read the following information and complete the activity instructions below.
Searching and filtering help API clients find the records they need without downloading every record. A search usually looks across text fields. A filter usually limits results by structured values such as category, availability, price, or order status.
For example, a shopper might want to find products in a specific category, products that mention wireless features, or products that are currently in stock. An administrator might want to find orders by status or review orders for one customer. Good searching and filtering makes the API more useful and reduces unnecessary data transfer.
Search and filter are different
A search usually asks, "Which records match this text?" A filter usually asks, "Which records match this exact condition?" A single endpoint can support both.
Query Design
Use clear query parameters that map to real client needs.
q: general text search.category: filter products by category.inStock: filter products by availability.minPriceandmaxPrice: filter products by a price range.status: filter orders by their current status.
GET /api/products?q=wireless&category=electronics&page=1&limit=10
This request searches for products that match the word wireless and are also in the electronics category. Because search results can still be large, search and filtering should usually work together with pagination.
Choosing Good Query Parameters
Query parameters should be predictable and consistent across endpoints. If q means general text search for products, it should mean the same thing for customers or orders if those endpoints support text search.
Good query parameters are:
- Clear:
categoryis easier to understand thanc. - Stable: avoid names that will need to change later.
- Limited: only support filters the endpoint can actually handle well.
- Documented: Swagger should show what parameters are allowed.
Examples by Endpoint
Different endpoints need different query options. Do not add every possible filter to every endpoint. Add filters that match the way clients will use the data.
| Endpoint | Useful Query Parameters |
|---|---|
GET /api/products |
q, category, inStock, minPrice, maxPrice |
GET /api/orders |
status, customerId, createdAfter, createdBefore |
GET /api/customers |
q, accountStatus, createdAfter, createdBefore |
GET /api/reviews |
productId, minRating, maxRating |
Exact Match Filters
Exact match filters are usually the simplest filters. They compare a field to one expected value.
const query = {};
if (req.query.category) {
query.category = req.query.category;
}
if (req.query.inStock === 'true') {
query.inStock = true;
}
if (req.query.inStock === 'false') {
query.inStock = false;
}
Exact match filters are useful for fields such as category, availability, order status, customer id, and product id.
Range Filters
Range filters are useful for numbers and dates. For example, a client might request products between $20 and $80, or orders created after a certain date.
if (req.query.minPrice || req.query.maxPrice) {
query.price = {};
if (req.query.minPrice) {
query.price.$gte = Number(req.query.minPrice);
}
if (req.query.maxPrice) {
query.price.$lte = Number(req.query.maxPrice);
}
}
In this query, $gte means greater than or equal to, and $lte means less than or equal to. When both values are present, MongoDB returns products whose price is at least the minimum price and no more than the maximum price. For example, minPrice=20&maxPrice=80 includes products priced at $20 and $80 as well as prices between them.
Range values must be parsed and validated. If a client sends minPrice=abc, the API should return a 400 response instead of sending a broken query to MongoDB.
Run the query
After the API has built and validated the query object, pass it to the Mongoose model. MongoDB applies every property in the object together, so a request can use an exact category filter, availability filter, and price range at the same time.
const products = await Product.find(query);
For example, if query contains a category of electronics and a price range, this call returns only electronics products within that range. Add pagination and sorting to the same query when the endpoint supports those features.
Text Search with MongoDB
For simple text search, use a MongoDB text index on the fields that should be searchable. A text index tells MongoDB which fields should be searched together when a client sends a search term such as q=wireless.
const productSchema = new mongoose.Schema({
name: String,
description: String,
tags: [String],
category: String
});
productSchema.index({
name: 'text',
description: 'text',
tags: 'text'
});
This schema is trimmed down to show only the fields involved in the text index. Your actual Product schema will also include fields such as price and inStock so the filters described above continue to work.
After the schema has a text index, your query can use $text and $search. You can still combine the text search with exact filters such as category.
const query = {};
if (req.query.category) {
query.category = req.query.category;
}
if (req.query.q !== undefined) {
if (typeof req.query.q !== 'string') {
return res.status(400).json({
errors: [{ field: 'q', message: 'Search text must be a single string.' }]
});
}
const searchText = req.query.q.trim();
if (!searchText || searchText.length > 100) {
return res.status(400).json({
errors: [{ field: 'q', message: 'Search text must be between 1 and 100 characters.' }]
});
}
query.$text = { $search: searchText };
}
const products = await Product.find(query).limit(limit).skip(skip);
This example searches the text-indexed fields and also applies the category filter if the client sends one. For this course, this is the preferred approach for simple text search in MongoDB.
Validate and normalize untrusted query input
Query parameters come from the client, so treat them as untrusted input. Before adding a search value to a database query, confirm that it has the expected type, remove unwanted leading and trailing whitespace, limit its length, and reject values that do not meet the endpoint's rules.
Build a new query object from accepted values instead of passing req.query directly to Mongoose. This keeps unsupported fields and unexpected query values out of the database query.
Validate query parameters
Search and filter values still need validation. Limit allowed sort fields, parse booleans and numbers carefully, and avoid passing raw query parameters directly into database operations.
Validation Rules
Before building a MongoDB query, decide which query parameters are allowed and what each one must look like.
const allowedCategories = ['electronics', 'home', 'clothing', 'books'];
const allowedInStockValues = ['true', 'false'];
if (req.query.category && !allowedCategories.includes(req.query.category)) {
return res.status(400).json({
errors: [{ field: 'category', message: 'category is not supported.' }]
});
}
if (req.query.inStock && !allowedInStockValues.includes(req.query.inStock)) {
return res.status(400).json({
errors: [{ field: 'inStock', message: 'inStock must be true or false.' }]
});
}
Validation is especially important for query parameters because clients can type anything into a URL.
No Matching Results
A search that finds no results should usually return 200 with an empty array, not 404. The endpoint exists and the request was valid; there simply were no matching records.
{
"data": [],
"query": {
"q": "wireless",
"category": "electronics"
},
"pagination": {
"page": 1,
"limit": 10,
"totalItems": 0,
"totalPages": 0
}
}
Use 404 when a client asks for one specific resource by id and that resource does not exist. Use an empty array when a list query has no matches.
Search Results
A search response should make it clear what filters were applied and how many records matched.
{
"data": [],
"query": {
"q": "wireless",
"category": "electronics"
},
"pagination": {
"page": 1,
"limit": 10,
"totalItems": 3,
"totalPages": 1
}
}
Including the applied query can make debugging easier. It also helps clients confirm that the server understood the filters correctly.
Combining Search, Filters, Sorting, and Pagination
Real list endpoints often combine several ideas:
- Search narrows results by text.
- Filters narrow results by exact or structured values.
- Sorting controls the order of the results.
- Pagination controls how many results are returned at once.
GET /api/products?q=wireless&category=electronics&sort=price&order=asc&page=1&limit=5
When multiple query features are combined, the API should apply all of them consistently. The total count should count the records that match the search and filters, not the total number of records in the whole collection.
Swagger Documentation
Swagger should document every supported search and filter parameter. Include allowed values when the parameter has a fixed set of choices.
parameters:
- name: q
in: query
schema:
type: string
description: Searches product name, description, and tags.
- name: category
in: query
schema:
type: string
enum: [electronics, home, clothing, books]
- name: inStock
in: query
schema:
type: string
enum: [true, false]
Testing Search and Filters
Useful search and filter tests include:
- A search term returns records that match expected text fields.
- A filter returns only records matching the filter.
- Multiple filters work together.
- No matches returns
200and an empty array. - Invalid filter values return
400. - Pagination metadata counts only the filtered results.
- Swagger documents each supported query parameter.
Activity Instructions
Check your understanding
- What is the difference between a search and a filter?
Click to expand the example answer
A search finds records that match text, such as a word in a product name or description. A filter limits records using a structured value, such as a product category or whether a product is in stock.
- Why should search and filter query parameters have clear, consistent names?
Click to expand the example answer
Clear and consistent names help API clients understand how to use an endpoint without learning a different pattern for every resource. They also make Swagger documentation easier to understand.
- Why must an API validate query parameters before using them in a MongoDB query?
Click to expand the example answer
Clients can send any value in a URL. Validation rejects unsupported categories, invalid booleans, or invalid price values with a clear
400response before they create unexpected database behavior. - What should a valid product search return when no products match?
Click to expand the example answer
It should return
200with an emptydataarray. The endpoint exists and the query was valid; no products simply matched it. - Why should a search endpoint count only records that match the search and filters when building pagination metadata?
Click to expand the example answer
The client needs metadata that describes the current result set. Counting every product in the collection would produce incorrect page totals and controls for a filtered search.
Other Links:
- Return to: Week Overview | Course Home