Build a JSON Database in Node.js (Part 4): Retrieve Records
65 min read·Dec 5, 2025
In this part, we'll implement a method for safely retrieving records from a table using a compact, intuitive query object, directly inspired from MongoDB's API.
Ready? Let's build!
Retrieve records from a table
Within the Model class, let's define a new method named find() responsible for retrieving, filtering, and ordering records from a table.
// ...
export default class Model {
// ...
find(filters = {}) {
//
}
}
This method takes as parameter a filters object with the following optional properties:
where <Object>: The list of attribute filters. If undefined, all records are returned.order <Array>: The sorting order of records based on an attribute's value. If undefined, records are returned in natural order.limit <number>: The maximum number of records. If undefined, all records matching thewhereclause are returned.attributes <Array>: The list of attributes to retrieve. If undefined, all attributes are included.
Make a shallow copy of records
In order to safely retrieve and successively filter records without mutating the original ones stored in the table, let's create a shallow copy of these records using the map() method.
// ...
export default class Model {
// ...
find(filters = {}) {
let results = this.#table.map(record => ({ ...record }));
}
}
Filter results based on attribute values
In its first form, the filters.where property is an optional object in the following format, that allows you to filter the records using a list of field conditions that logically compare attributes to arbitrary values:
find({
where: {
attribute: { $operator: value, ... },
...
}
})
Where:
-
attribute <string>: The name of an attribute (e.g.,'price'). -
$operator <string>: A comparison operator (e.g.,'$gte'). Must be one of:$eq: equal (=)$neq: not equal (!=)$gt: greater than (>)$gte: greater or equal (>=)$lt: less than (<)$lte: less or equal (<=)$in: in ([])$nin: not in (![])
-
value <any>: An arbitrary value to compare the attribute to using the specifiedoperator(e.g.,1000).
👉 When using this syntax, all the field conditions must evaluate to
truefor the record to be included in the result set, which is equivalent to using the logical AND operator.
In its second form, the filters.where property is an optional object in the following format primarily inspired from MongoDB's query language, that allows you to filter the records using logical groups that combine field conditions:
find({
where: {
$group: [
{ attribute: { $operator: value, ... } },
...
]
}
})
Where $group is either:
$and <Array[<Object>]>: A list of field conditions, where all must evaluate totruefor the record to be included in the result set (e.g.,$and: [{}, ...]).$or <Array[<Object>]>: A list of field conditions, where at least one must evaluate totruefor the record to be included in the result set (e.g.,$or: [{}, ...]).
When using this syntax, you can also nest logical groups the following way:
find({
where: {
$group: [
{ attribute: { $operator: value } },
{ $group: [
{ attribute: { $operator: value } },
] }
]
}
})
Example 1
Let's consider the following query:
Freelancers.find({
where: {
status: { $eq: 'active' },
daily_rate: { $gte: 400 }
}
});
Which is equivalent to the following query:
Freelancers.find({
where: {
$and: [
{ status: { $eq: 'active' } },
{ daily_rate: { $gte: 400 } }
]
}
});
Which in plain English translates to:
"Retrieve all the records from the
freelancerstable where thestatusattribute is equal to'active'and thedaily_rateattribute is greater than or equal to400".
And in SQL to:
SELECT * FROM freelancers WHERE status = 'active' AND daily_rate >= 400;
Example 2
Let's consider the following query:
Freelancers.find({
where: {
daily_rate: { $gte: 400, $lt: 650 }
}
});
Which in plain English translates to:
"Retrieve all the records from the
freelancerstable where thedaily_rateattribute is greater than or equal to400and less than650".
And in SQL to:
SELECT * FROM freelancers WHERE daily_rate >= 400 AND daily_rate < 650;
Example 3
Let's consider the following query:
Freelancers.find({
where: {
$and: [
{ status: { $eq: 'active' } },
{
$or: [
{ city: { $eq: 'Quebec' } },
{ city: { $eq: 'Montreal' } }
]
}
],
}
});
Which in plain English translates to:
"Retrieve all the records from the
freelancerstable, where thestatusattribute is equal to'active', and where thecityattribute is either equal to'Quebec'or'Montreal'.
And in SQL to:
SELECT * FROM freelancers WHERE status = 'active' AND (city = 'Quebec' OR city = 'Montreal');
Example 4
Let's consider the following query:
Freelancers.find({
where: {
$or: [
{
$and: [
{ city: { $eq: 'Quebec' } },
{ daily_rate: { $lte: 800 } }
]
},
{
$and: [
{ city: { $eq: 'Montreal' } },
{ daily_rate: { $lt: 500 } }
]
}
]
}
});
Which in plain English translates to:
"Retrieve all the records from the
freelancerstable, where thecityattribute is equal to'Quebec'and thedaily_rateattribute is less than or equal to800, or where thecityattribute is equal to'Montreal'and thedaily_rateattribute is less than500.
And in SQL to:
SELECT * FROM freelancers WHERE (city = 'Quebec' AND daily_rate <= 800) OR (city = 'Montreal' AND daily_rate < 500);
Evaluate field conditions
Let's define a new private method named #applyCondition() in the Model class responsible for logically evaluating a field condition:
// ...
export default class Model {
// ...
#applyCondition(attrValue, opSet) {
//
}
// ...
}
This method takes as parameters:
attrValue <any>: The attribute's value to compare (e.g.,650).opSet <Object>: The list of logical operator sets to compare the attribute's value to (e.g.,{ $gte: 450, $lt: 800 }).
Within this method, let's throw an error if the list of operator sets is not an object.
// ...
export default class Model {
// ...
#applyCondition(attrValue, opSet) {
if (!isPlainObject(opSet)) {
throw new Error(`Model: #applyCondition(): "opSet" must be an object of operators`);
}
}
// ...
}
And let's:
- Iterate on every operator set.
- Translate each set into a logical expression and immediately return
falseif the expression evaluates tofalse, otherwise returntrue. - Throw an error if the operator of a set is invalid.
// ...
export default class Model {
// ...
#applyCondition(attrValue, opSet) {
// ...
for (const [operator, operand] of Object.entries(opSet)) {
switch (operator) {
case '$eq':
if (attrValue !== operand) return false;
break;
case '$neq':
if (attrValue === operand) return false;
break;
case '$gt':
if (!(attrValue > operand)) return false;
break;
case '$gte':
if (!(attrValue >= operand)) return false;
break;
case '$lt':
if (!(attrValue < operand)) return false;
break;
case '$lte':
if (!(attrValue <= operand)) return false;
break;
case '$in':
if (!Array.isArray(operand)) {
throw new Error(`Model: find(): "filters.where.$in" must be an array`);
} else if (!operand.includes(attrValue)) {
return false;
}
break;
case '$nin':
if (!Array.isArray(operand)) {
throw new Error(`Model: find(): "filters.where.$nin" must be an array`);
} else if (operand.includes(attrValue)) {
return false;
}
break;
default:
throw new Error(`Model: #applyCondition(): unknown operator "${operator}"`);
}
}
return true;
}
// ...
}
Evaluate logical operations
Let's define a new private method named #evalFilters() in the Model class responsible for evaluating the filters.where parameter object:
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
//
}
// ...
}
This method takes as parameters:
record <Object>: The record to filter.filters <Object>: The list of filters from thefilters.whereparameter the record's attributes must match.
Within this method, let's:
- Return
trueif thefiltersparameter isundefined. - Otherwise, throw an error if the
filtersparameter is not an object.
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
if (filters === undefined) {
return true;
} else if (!isPlainObject(filters)) {
throw new Error(`Model: find(): "filters.where" must be an object`);
}
}
// ...
}
To first evaluate the field conditions present in the filters object, let's
- Iterate on its properties.
- Throw an error if the key is not a valid logical operator (i.e.,
$and,$or). - Skip the key for now if it is a logical operator.
- Otherwise, execute the
#applyCondition()method and returnfalseif it returnsfalse.
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
// ...
for (const [key, expression] of Object.entries(filters)) {
if (key.startsWith('$') && key !== '$and' && key !== '$or') {
throw new Error(`Model: find(): unknown logical operator "${key}" in "filters.where"`);
} else if (key === '$and' || key === '$or') {
continue;
} else if (!this.#applyCondition(record[key], expression)) {
return false;
}
}
}
// ...
}
Let's check if the logical AND group is defined and:
- Throw an error if the
$andproperty is not a non-empty array. - Otherwise, recursively evaluate each field condition and return
falseif any returnsfalse.
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
// ...
if (filters.$and !== undefined) {
if (!Array.isArray(filters.$and) || filters.$and.length === 0) {
throw new Error(`Model: find: "filters.where.$and" must be a non-empty array`);
} else if (!filters.$and.every(subfilters => this.#evalFilters(record, subfilters))) {
return false;
}
}
}
// ...
}
💡 In JavaScript, the
every()method returnsfalseif it finds one element in the array that does not satisfy the provided testing function. Otherwise, it returnstrue.
Let's check if the logical OR group is defined and:
- Throw an error if the
$orproperty is not a non-empty array. - Otherwise, recursively evaluate each field condition and return
falseif all returnfalse.
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
// ...
if (filters.$or !== undefined) {
if (!Array.isArray(filters.$or) || filters.$or.length === 0) {
throw new Error(`Model: find: "filters.where.$or" must be a non-empty array`);
} else if (!filters.$or.some(subfilters => this.#evalFilters(record, subfilters))) {
return false;
}
}
}
// ...
}
💡 In JavaScript, the
some()method returnstrueif it finds one element in the array that satisfies the provided testing function. Otherwise, it returnsfalse.
Last but not least, let's return true if none of the previous evaluations returned false.
// ...
export default class Model {
// ...
#evalFilters(record, filters) {
// ...
return true;
}
// ...
}
Finally, within the find() method, let's:
- Use the
#evalFilters()method to filter out the records from the result set that don't match the list of filters defined in thefilters.whereparameter. - Return the
resultsarray.
// ...
export default class Model {
// ...
find(filters = {}) {
let results = this.#table
.map(record => ({ ...record }))
.filter(result => this.#evalFilters(result, filters.where));
return results;
}
}
Conclusion
Congratulations!
You now have a method that uses an intuitive query object to retrieve and filter records.
In the next part, you'll learn how to extend this method's capabilities in order to sort, limit, and filter records.
Read next: Build a JSON Database in Node.js (Part 5): Order, Limit & Filter Records