Build a JSON Database in Node.js (Part 3): Insert & Persist Records
22 min read·Dec 5, 2025
In this part, we'll implement a method for inserting a new record into a database table and persist these changes to the disk.
Ready? Let's build!
Insert a record into a table
Within the Model class, let's declare a create() method responsible for:
- Validating and sanitizing the properties of a data object against the table's validation schema.
- Inserting the object as a new record into the table.
// ...
export default class Model {
// ...
create(record = {}) {
//
}
}
This method takes as parameter:
record <Object>: A data object that represents a record to insert into the table.
Let's:
- Import the
isPlainObject()function from theutils.jsmodule. - Throw an error if the
recordparameter is not an object.
import Joi from 'joi';
import { isPlainObject } from './utils.js';
export default class Model {
// ...
create(record = {}) {
if (!isPlainObject(record)) {
throw new Error(`Model: create(): "record" is not an object`);
}
}
}
Let's:
-
Invoke the
validate()method of the#schemaprivate class field -
Use the destructuring syntax to unpack the properties returned by this method, where:
value <Object>: The validated, sanitized version of the attributes after applying the schema (e.g., type coercions, defaults, etc).error <ValidationError>|<undefined>: AValidationError(orundefinedif the attributes are valid) describing any rule violations and invalid properties.
// ...
export default class Model {
// ...
create(record = {}) {
if (!isPlainObject(record)) {
throw new Error(`Model: create(): "record" is not an object`);
}
const { value, error } = this.#schema.validate(record, { abortEarly: false });
}
}
💡 Setting the
abortEarlyflag tofalsewill cause thevalidate()method to output the complete list of validation error for each invalid property of the target object, rather than just the first invalid one.
Let's throw an error that includes the message property of the ValidationError object if the error variable is defined.
// ...
export default class Model {
// ...
create(record = {}) {
if (!isPlainObject(record)) {
throw new Error(`Model: create(): "record" is not an object`);
}
const { value, error } = this.#schema.validate(record, { abortEarly: false });
if (error) {
throw new Error(`Model: create(): ${error.message}`);
}
}
}
Otherwise, let's:
- Update the
recordobject with the sanitized properties of thevalueobject. - Insert the
recordobject into the model's table. - Return the
recordobject.
// ...
export default class Model {
// ...
create(record = {}) {
if (!isPlainObject(record)) {
throw new Error(`Model: create(): "record" is not an object`);
}
const { value, error } = this.#schema.validate(record, { abortEarly: false });
if (error) {
throw new Error(`Model: create(): ${error.message}`);
}
record = { ...value };
this.#table.push(record);
return record;
}
}
Persist database changes on disk
At the moment, the database engine only keeps its data in memory — this means that any newly created records will be erased as soon as the process crashes, restarts, or exits.
To avoid data loss, let's declare a private asynchronous method named saveToDisk() in the RowStackDB class responsible for writing the content of in memory tables into the JSON database file.
// ...
export default class RowStackDB {
// ...
async #saveToDisk() {
//
}
}
To ensure that every write to the disk is atomic — in other words "complete-or-nothing" — the #saveToDisk() method will:
- Write the serialized tables into a temporary database file.
- Ensure that all the data has been entirely written to the file.
- Overwrite the original database file by renaming the temporary one.
Within this method, let's create a snapshot of the database tables using the JSON.stringify() method to ensure that the data won't change mid-write and be potentially corrupted.
// ...
export default class RowStackDB {
// ...
async #saveToDisk() {
const snapshot = JSON.stringify({ tables: this.#tables }, null, 2);
}
}
Let's:
- Import the promise-based version of the code Node.js
fsmodule - Use the
fsp.open()method within atry...catchblock to open the temporary database file named<database>.tmp.jsonin write-only mode.
// ...
import * as fsp from 'node:fs/promises';
export default class RowStackDB {
// ...
async #saveToDisk() {
const snapshot = JSON.stringify({ tables: this.#tables }, null, 2);
const tmpFile = `${this.#dirPath}/${this.#dbName}.tmp.json`;
let filehandle;
try {
filehandle = await fsp.open(tmpFile, 'w');
} catch(error) {
throw new Error(`RowStackDB: #saveToDisk(): ${error.message}`);
}
}
}
Let's:
- Use the
fsp.writeFile()method of the file handle returned by the call to thefsp.open()method to write the snapshot into the temporary database file - Use the
fsp.sync()method to ensure that all the data stored in the OS buffer has been written into the file. - Use the
fsp.close()method to close the file and free all the resources attached to it.
// ...
export default class RowStackDB {
// ...
async #saveToDisk() {
const snapshot = JSON.stringify({ tables: this.#tables }, null, 2);
const tmpFile = `${this.#dirPath}/${this.#dbName}.tmp.json`;
let filehandle;
try {
filehandle = await fsp.open(tmpFile, 'w');
await filehandle.writeFile(snapshot, 'utf8');
await filehandle.sync();
} catch(error) {
throw new Error(`RowStackDB: #saveToDisk(): ${error.message}`);
} finally {
if (filehandle) {
await filehandle.close();
}
}
}
}
Last but not least, let's use the fsp.rename() method to atomically overwrite the original database file by renaming the temporary one we've just written.
// ...
export default class RowStackDB {
// ...
async #saveToDisk() {
// ...
try {
await fsp.rename(tmpFile, this.#dbPath);
} catch(error) {
throw new Error(`RowStackDB: #saveToDisk(): ${error.message}`);
}
}
}
Forward the #saveToDisk() method
Within the define() method of the RowStackDB class, let's pass the #saveToDisk() method as argument to the constructor() method of the Model class to make it down the line available to the create() method.
// ...
export default class RowStackDB {
// ...
define(name, attributes) {
// ...
return new Model(
this.#tables[name],
this.#schemas[name],
() => this.#saveToDisk()
);
}
}
Within the constructor() method of the Model class, let's:
- Update its signature to account for the new parameter we've just added.
- Assign this parameter to a new private class field named
#saveToDisk.
// ...
export default class Model {
#table;
#schema;
#saveToDisk;
constructor(table, schema, saveToDisk) {
if (!Array.isArray(table)) {
throw new Error(`Model: constructor(): "table" must be an array`);
} else if (!Joi.isSchema(schema)) {
throw new Error(`Model: constructor(): "schema" must be a validation schema`);
} else if (typeof saveToDisk !== 'function') {
throw new Error(`Model: constructor(): "saveToDisk" must be a function`);
}
this.#table = table;
this.#schema = schema;
this.#saveToDisk = saveToDisk;
}
// ...
}
Finally, within the create() method of the Model class, let's execute the #saveToDisk() method right before the return statement to persist the database changes.
// ...
export default class Model {
// ...
async create(record = {}) {
// ...
await this.#saveToDisk();
return record;
}
}
Conclusion
Congratulations!
You now have a database layer that exposes an API with a method for validation, sanitizing, and inserting a record into a table.
In the next part, you'll learn how to implement a method for safely retrieving and filtering records from a table using a MongoDB-like query object.
Read next: Build a JSON Database in Node.js (Part 4): Retrieve Records
Unlock the program 🚀
Pay once, own it forever.
€79
30-day money-back guarantee
- 13 modules
- 113 lessons with full-code examples
- 29 projects with commented solutions
- All future lesson and project updates
- Lifetime access
By submitting this form, you agree to the Terms & Conditions and Privacy Policy.