Build a JSON Database in Node.js (Part 7): Delete Records Softly & Hardly
36 min read·Dec 5, 2025
In this part, we'll implement a method for deleting records, including soft deletion by default.
Ready? Let's build!
Delete records
Within the Model class, let's define a new method named delete() responsible for deleting records from the associated model's table:
// ...
export default class Model {
// ...
delete(filters = {}) {
//
}
}
This method takes as a single parameter:
filters <Object>: The list of filters identical to thefilters.whereobject used in thefind()method.
Example
Let's consider the following query:
await Emails.delete({
folder: { $eq: 'bin' },
read: { $eq: false }
});
Which in plain English translates to:
"Delete all the records from the
emailstable wherefolderequals'bin'andreadtofalse."
And in SQL to:
DELETE FROM emails WHERE folder = 'bin' AND read = FALSE;
Check the method's parameters
Within this method, let's throw an error if the filters parameter isn't an object.
// ...
export default class Model {
// ...
delete(filters = {}) {
if (!isPlainObject(filters)) {
throw new Error('Model: delete(): "filters" must be an object');
}
}
}
Filter records
Let's iterate on the records of the model's table in reverse order to keep the indices of the not-yet-visited records unchanged and use the #evalFilters() method to check if the current record matches the list of filters.
// ...
export default class Model {
// ...
delete(filters = {}) {
// ...
for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
let record = this.#table[i];
if (this.#evalFilters(record, filters)) {
//
}
}
}
}
Let's:
- Declare a
deletedCountvariable to keep track of the number of records deleted. - Remove the current record from the table if the list of filters are a match.
- Increment the deletions counter by 1.
// ...
export default class Model {
// ...
delete(filters = {}) {
// ...
let deletedCount = 0;
for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
let record = this.#table[i];
if (this.#evalFilters(record, filters)) {
this.#table.splice(i, 1);
deletedCount++;
}
}
}
}
Finally, let's:
- Declare the
delete()method as asynchronous. - Use the
#saveToDisk()method to persist the changes on disk if at least one record has been deleted. - Return the deletions counter.
// ...
export default class Model {
// ...
async delete(filters = {}) {
// ...
if (deletedCount > 0) {
await this.#saveToDisk();
}
return deletedCount;
}
}
Enable soft deletions
Soft deletion is the practice of marking records as deleted instead of physically and irreversibly removing them, which in turn allows developers to undo deletions whenever needed and keep data for audits/history.
Update the define() method
To implement this mechanism, let's first update the define() method of the RowStackDB class by adding an is_deleted property to the validation schema object, with a default value set to false.
// ...
export default class RowStackDB {
// ...
define(name, attributes) {
// ...
this.#schemas[name] = Joi.object({
...schema,
is_deleted: Joi.boolean().default(false).required()
});
}
// ...
}
Update the create() method
Let's update the create() method of the Model class by adding an is_deleted property (set to false by default) to the validated object to account for the same addition in the validation schema object.
// ...
export default class Model {
// ...
async create(record = {}) {
// ...
const { value, error } = this.#schema.validate({
...record,
is_deleted: false
}, {
abortEarly: false
});
// ...
}
}
Update the find() method
Let's update the find() method of the Model class by filtering out the records whose is_deleted property is set to true, and remove this property from the objects in the results set to keep it "hidden".
// ...
export default class Model {
// ...
find(filters = {}) {
let results = this.#table
.map(record => ({ ...record }))
.filter(result => !result.is_deleted && this.#evalFilters(result, filters.where))
.map(({ is_deleted, ...result }) => result);
// ...
}
}
Update the update() method
Let's update the update() method of the Model class by filtering out the records whose is_deleted property is set to true.
// ...
export default class Model {
// ...
async update(filters = {}, values = {}) {
// ...
for (let record of this.#table) {
if (!record.is_deleted && this.#evalFilters(record, filters)) {
// ...
}
}
// ...
}
}
Update the delete() method
Let's:
- Update the signature of the
delete()method by adding a new parameter namedoptionswith a default property namedsoftset totrueused to mark records as "softly deleted" by default. - Throw an error if the
optionsparameter is not an object.
// ...
export default class Model {
// ...
async delete(filters = {}, options = { soft: true }) {
if (!isPlainObject(filters)) {
throw new Error('Model: delete(): "filters" must be an object');
} else if (!isPlainObject(options)) {
throw new Error('Model: delete(): "options" must be an object');
}
// ...
}
}
Finally, let's:
- Filter out the records whose
is_deletedproperty is set totrue. - Physically remove the matching records from the table if the
options.softproperty is set tofalse. - Otherwise, set their
is_deletedproperty totrue.
// ...
export default class Model {
// ...
async delete(filters = {}, options = { soft: true }) {
// ...
for (let i = this.#table.length - 1 ; i >= 0 ; i--) {
let record = this.#table[i];
if (!record.is_deleted && this.#evalFilters(record, filters)) {
if (options.soft === false) {
this.#table.splice(i, 1);
} else {
record.is_deleted = true;
}
deletedCount++;
}
}
// ...
}
}
Conclusion
Congratulations!
You now have a fully functional database engine with an API that allows you to define tables and perform CRUD operations on them.