Build a JSON Database in Node.js (Part 3): Insert & Persist Records

22 min read·Dec 5, 2025

Article banner

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:

  1. Validating and sanitizing the properties of a data object against the table's validation schema.
  2. Inserting the object as a new record into the table.
model.js
// ...

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:

  1. Import the isPlainObject() function from the utils.js module.
  2. Throw an error if the record parameter is not an object.
model.js
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:

  1. Invoke the validate() method of the #schema private class field

  2. 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>: A ValidationError (or undefined if the attributes are valid) describing any rule violations and invalid properties.
model.js
// ...

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 abortEarly flag to false will cause the validate() 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.

model.js
// ...

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:

  1. Update the record object with the sanitized properties of the value object.
  2. Insert the record object into the model's table.
  3. Return the record object.
model.js
// ...

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.

engine.js
// ...

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:

  1. Write the serialized tables into a temporary database file.
  2. Ensure that all the data has been entirely written to the file.
  3. 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.

engine.js
// ...

export default class RowStackDB {
  // ...

  async #saveToDisk() {
    const snapshot = JSON.stringify({ tables: this.#tables }, null, 2);
  }
}

Let's:

  1. Import the promise-based version of the code Node.js fs module
  2. Use the fsp.open() method within a try...catch block to open the temporary database file named <database>.tmp.json in write-only mode.
engine.js
// ...
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:

  1. Use the fsp.writeFile() method of the file handle returned by the call to the fsp.open() method to write the snapshot into the temporary database file
  2. Use the fsp.sync() method to ensure that all the data stored in the OS buffer has been written into the file.
  3. Use the fsp.close() method to close the file and free all the resources attached to it.
engine.js
// ...

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.

engine.js
// ...

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.

engine.js
// ...

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:

  1. Update its signature to account for the new parameter we've just added.
  2. Assign this parameter to a new private class field named #saveToDisk.
model.js
// ...

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.

model.js
// ...

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.