Build a JSON Database in Node.js (Part 1): Project Setup & File Persistence
40 min read·Dec 5, 2025
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 tomodulein 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.
export default class RowStackDB {
//
}
Initialize the database storage directory and files
Within the RowStackDB class, let's declare a constructor() method responsible for:
- Creating a local directory for storing database files.
- Creating a local JSON database file or loading its content in memory.
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.
function isNonEmptyString(value) {
return typeof value === 'string' && value.length > 0;
}
export {
isNonEmptyString
};
Within the constructor() method, let's:
- Import the
isNonEmptyString()function from theutils.jsmodule. - Throw an error if the
dbNameparameter is not a non-empty string. - Otherwise, store its value into a private class field named
#dbName.
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:
- Import the core Node.js
pathmodule that provides utilities for working with paths. - Throw an error if the
dirPathparameter is not a non-empty string. - Otherwise, use the
path.resolve()method to convert the value of thedirPathparameter into an absolute path and store it into a private class field named#dirPath. - Store the full path to the database file into a private class field named
#dbPath, without forgetting to postfix it with the.jsonfile extension.
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:
- Import the core Node.js
fsmodule that enables to interact with the file system. - Use the
fs.mkdirSync()method to create the database storage directory.
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
recursiveflag totruewill cause thefs.mkdirSync()method to fail silently if the directory already exists.
Create the database file
Let's:
- Declare a new object literal named
dbthat contains a single property namedtablesthat will be used to store the database tables (and their records) in memory. - Use the
fs.existsSync()method to check if the database file doesn't exist - Use the
fs.writeFileSync()method combined with theJSON.stringify()method to create the file, and write thedbobject into it in the form of a JSON string.
// ...
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:
- Use the
fs.readFileSync()method to read the content of the database file. - Use the
JSON.parse()method to convert it into a JavaScript object. - Assign the parsed object back to the
dbvariable.
// ...
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.
// ...
function isPlainObject(value) {
return value !== null && typeof value === 'object' && !Array.isArray(value);
}
export {
isNonEmptyString,
isPlainObject,
};
💡 In JavaScript, the type of the
nullvalue is an object.
Finally, let's:
- Import the
isPlainObject()function from theutils.jsmodule. - Throw an error if the
dbvariable is not an object. - Throw an error if the
db.tablesproperty is not an object. - Otherwise, store the value of the
db.tablesproperty into a private class field named#tables.
// ...
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.
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