Build a JSON Database in Node.js (Part 1): Project Setup & File Persistence

40 min read·Dec 5, 2025

Article banner

In this first part, we'll lay the foundation of RowStackDB by scaffolding the project, creating the database engine module, and implementing a method for initializing the database storage directory and files.

Ready? Let's build!

The relational database model

A relational database is a type of database based on the relational model which is a way of representing data into tables also called relations, where the rows of these tables are designated as records and the columns as attributes.

A record usually represents the instance of an entity type, such as a customer or a product, whereas an attribute will hold a value of that instance like for example a name, an email address, a phone number or a quantity.

Our JSON database will follow this model using:

  • Relations (or tables) as arrays.
  • Records (or rows) as objects.
  • Attributes (or columns) as properties of these objects.

Here's an example of a JSON database file with a single table named users containing a single record:

{
  "tables": {
    "users": [
      {
        "email_address": "john.snow@example.com",
        "full_name": "John Snow",
        "account_type": "premium",
        "status": "active",
        "city": "Quebec",
        "created_at": "2025-11-17T11:12:05.654Z",
        "is_deleted": false
      }
    ]
  }
}

Set up the project

Create the project's directory

Let's create a new directory for the project named rowstackdb and enter it:

~/code-in-action$ mkdir rowstackdb
~/code-in-action$ cd rowstackdb

Create the project's manifest

Let's create the package.json file of the project using the npm init command and answer each question the following way:

~/code-in-action/rowstackdb$ npm init
package name: (rowstackdb) 
version: (1.0.0)
description: A lightweight relational JSON database written in Node.js
entry point: (index.js) 
test command: 
git repository: 
keywords: json database orm
author: Razvan Ludosanu
license: (ISC) MIT
type: (commonjs) module

👉 Don't forget to set the "type" property to module in order to be able to use the ECMAScript module syntax.

Install the project's dependencies

Let's install the joi package for creating validation schema objects:

~/code-in-action/rowstackdb$ npm install joi

Create the database engine module

The engine (a.k.a. storage engine) is the core database component that owns the database's files and in-memory state, coordinates reads/writes, enforces rules, and exposes an API to higher layers while providing durability/atomicity.

Let's create a new module named engine.js, and within it, declare and export a class named RowStackDB.

engine.js
export default class RowStackDB {
  //
}

Initialize the database storage directory and files

Within the RowStackDB class, let's declare a constructor() method responsible for:

  1. Creating a local directory for storing database files.
  2. Creating a local JSON database file or loading its content in memory.
engine.js
export default class RowStackDB {
  constructor(dbName, dirPath = 'data') {
    //
  }
}

This method takes as parameters:

  • dbName <string>: The database filename (e.g., 'learn_backend').
  • dirPath <string>: The path to the local database storage directory (e.g., ./storage/databases). Default: ./data.

Check the database name

Let's create a new module named utils.js that will be used to store helper functions, and within it, export a function named isNonEmptyString() that returns true if a value is string with at least one character, and false otherwise.

utils.js
function isNonEmptyString(value) {
  return typeof value === 'string' && value.length > 0;
}

export {
  isNonEmptyString
};

Within the constructor() method, let's:

  1. Import the isNonEmptyString() function from the utils.js module.
  2. Throw an error if the dbName parameter is not a non-empty string.
  3. Otherwise, store its value into a private class field named #dbName.
engine.js
import { isNonEmptyString } from './utils.js';

export default class RowStackDB {
  #dbName;

  constructor(dbName, dirPath = 'data') {
    if (!isNonEmptyString(dbName)) {
      throw new Error(`RowStackDB: constructor(): "dbName" name must be a non-empty string`);
    }

    this.#dbName = dbName;
  }
}

Check the database storage directory path

Let's:

  1. Import the core Node.js path module that provides utilities for working with paths.
  2. Throw an error if the dirPath parameter is not a non-empty string.
  3. Otherwise, use the path.resolve() method to convert the value of the dirPath parameter into an absolute path and store it into a private class field named #dirPath.
  4. Store the full path to the database file into a private class field named #dbPath, without forgetting to postfix it with the .json file extension.
engine.js
import * as path from 'node:path';
import { isNonEmptyString } from './utils.js';

export default class RowStackDB {
  #dbName;
  #dirPath;
  #dbPath;

  constructor(dbName, dirPath = 'data') {
    if (!isNonEmptyString(dbName)) {
      throw new Error(`RowStackDB: constructor(): "dbName" name must be a non-empty string`);
    } else if (!isNonEmptyString(dirPath)) {
      throw new Error(`RowStackDB: constructor(): "dirPath" name must be a non-empty string`);
    }
    
    this.#dbName = dbName;
    this.#dirPath = path.resolve(dirPath);
    this.#dbPath = path.join(this.#dirPath, `${this.#dbName}.json`);
  }
}

Create the database storage directory

Let's:

  1. Import the core Node.js fs module that enables to interact with the file system.
  2. Use the fs.mkdirSync() method to create the database storage directory.
engine.js
import * as path from 'node:path';
import * as fs from 'node:fs';
import { isNonEmptyString } from './utils.js';

export default class RowStackDB {
  #dbName;
  #dirPath;
  #dbPath;

  constructor(dbName, dirPath = 'data') {
    // ...

    fs.mkdirSync(this.#dirPath, { recursive: true });
  }
}

💡 Setting the recursive flag to true will cause the fs.mkdirSync() method to fail silently if the directory already exists.

Create the database file

Let's:

  1. Declare a new object literal named db that contains a single property named tables that will be used to store the database tables (and their records) in memory.
  2. Use the fs.existsSync() method to check if the database file doesn't exist
  3. Use the fs.writeFileSync() method combined with the JSON.stringify() method to create the file, and write the db object into it in the form of a JSON string.
engine.js
// ...

export default class RowStackDB {
  #dbName;
  #dirPath;
  #dbPath;

  constructor(dbName, dirPath = 'data') {
    // ...

    let db = { tables: {} };

    if (!fs.existsSync(this.#dbPath)) {
      fs.writeFileSync(this.#dbPath, JSON.stringify(db, null, 2));
    }
  }
}

Load and parse the database file

Within the engine.js module, let's:

  1. Use the fs.readFileSync() method to read the content of the database file.
  2. Use the JSON.parse() method to convert it into a JavaScript object.
  3. Assign the parsed object back to the db variable.
engine.js
// ...

export default class RowStackDB {
  // ...

  constructor(dbName, dirPath = 'data') {
    // ...

    let db = { tables: {} };

    if (!fs.existsSync(this.#dbPath)) {
      fs.writeFileSync(this.#dbPath, JSON.stringify(db, null, 2));
    } else {
      db = JSON.parse(fs.readFileSync(this.#dbPath, { encoding: 'utf8' }));
    }
  }
}

Let's implement and export a new helper function within the utils.js module named isPlainObject() that returns true if a value is a plain object, and false otherwise.

utils.js
// ...

function isPlainObject(value) {
  return value !== null && typeof value === 'object' && !Array.isArray(value);
}

export {
  isNonEmptyString,
  isPlainObject,
};

💡 In JavaScript, the type of the null value is an object.

Finally, let's:

  1. Import the isPlainObject() function from the utils.js module.
  2. Throw an error if the db variable is not an object.
  3. Throw an error if the db.tables property is not an object.
  4. Otherwise, store the value of the db.tables property into a private class field named #tables.
engine.js
// ...
import { isNonEmptyString, isPlainObject } from './utils.js';

export default class RowStackDB {
  // ...
  #tables;

  constructor(dbName, dirPath = 'data') {
    // ...

    if (!isPlainObject(db)) {
      throw new Error(`RowStackDB: constructor(): "db" must be an object`);
    } else if (!isPlainObject(db.tables)) {
      throw new Error(`RowStackDB: constructor(): "db.tables" must be an object`);
    } else {
      this.#tables = db.tables;
    }
  }
}

Export the engine module

Let's create a new module named index.js that will serve as a "barrel" for the database modules, and within it, export the engine.js module as RowStackDB.

index.js
export { default as RowStackDB } from './engine.js';

Conclusion

Congratulations!

You now have a basic database engine that can initialize a storage directory, create new database files, and parse the content of existing database files in the JSON format.

In the next part, you'll learn how to define database tables, enforce their structure using validation schemas, and define an ORM-like layer that will later on provide methods for performing CRUD operations on tables.

Read next: Build a JSON Database in Node.js (Part 2): Table Definition & Data Integrity