# node-json-db: a JSON file used as a small database

> node-json-db stores JavaScript objects in a JSON file and addresses them with a path syntax modelled on XPath, with every call asynchronous and a configuration object that decides when the file is written. It is aimed at small Node.js projects that need persistence without a database server, and it is explicit about where that stops being a good idea.

**Belphemur/node-json-db** — A simple "database" that use JSON file for Node.JS.

- Repository: https://github.com/Belphemur/node-json-db
- Stars: 830 · Forks: 66
- Language: TypeScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/belphemur-node-json-db

## The whole database is one JSON file

node-json-db describes itself as a simple database that uses a JSON file for Node.js, and the inner working section is blunt about what that means: the module stores the data using JavaScript objects directly into a JSON file.

There is no engine, no index, no query planner and no server. What you get is a document store with a path language, and the value of the library is in the ergonomics it adds around that idea rather than in anything it does underneath.

The paths are the interesting part. The DataPath follows the same principle as XPath, and you traverse the stored object to reach a property. Given a document shaped like a test object with a data1 member holding an array and a data2 member holding the number five, the array is at the path for test, data1 and array in turn, the number is at the path for test and data2, and the whole test object is simply the path for test. The root of the document is the path with a single slash and nothing after it.

So the mental model is XPath for JSON, which is a good deal more useful than the alternative most small libraries offer, which is a key value store with a flat namespace.

The project is MIT licensed, published on npm as node-json-db, and the repository carries a codecov badge and a Node.js workflow badge, with the current version at 2.6.0.

## The Config object decides everything about the file

The constructor takes a Config object rather than loose arguments, and that was a deliberate change in version 2.0.

The README has a breaking changes section for the 2.x line, and it lists two consequences. You are now forced to use the Config object to set up JsonDB. And every method is now asynchronous.

The reason given is in the design rather than in style: the library reads from the database file on demand, and depending on how the database is configured, it might write at each push. If either of those can be a file operation, the API has to be async.

The configuration itself has five fields, and the README documents each one in comments in the example.

```javascript
import { JsonDB, Config } from "node-json-db";

// The first argument is the database filename. If no extension is used, '.json' is assumed and automatically added.
// The second argument is used to tell the DB to save after each push
// If you set the second argument to false, you'll have to call the save() method.
// The third argument is used to ask JsonDB to save the database in a human readable format. (default false)
// The fourth argument is the separator. By default it's slash (/)
// The fifth argument enables sync writes (default false)
var db = new JsonDB(new Config("myDataBase", true, false, "/", false));
```

The filename is the first, and if you leave off the extension the .json suffix is added for you. The second decides whether the database is saved after each push, and if you set it to false you have to call save yourself. The third asks for human readable output, which is off by default, and that flag is the difference between a compact machine file and something you can read in a diff. The fourth is the separator, a slash by default, and the fifth enables synchronous writes.

That last flag deserves a note. Synchronous writes block the event loop, which is exactly what you want in a short lived script that must not exit before the data is on disk, and exactly what you do not want in a server.

The library is installed with pnpm or npm, and the documentation is auto generated with TypeDoc.

## Pushing data, merging and the primitives rule

The push method is the write side, and it takes a path, a value and a flag that switches between overwrite and merge.

By default a push overrides the old value at that path. When you push to a path that does not exist, the hierarchy is created for you, so a push to a nested path on an empty database produces the intermediate objects rather than failing. You can also push an object directly.

```javascript
// Pushing the data into the database
// With the wanted DataPath
// By default the push will override the old value
await db.push("/test1", "super test");

// When pushing new data for a DataPath that doesn't exist, it automatically creates the hierarchy
await db.push("/test2/my/test", 5);

// You can also push objects directly
await db.push("/test3", { test: "test", json: { test: ["test"] } });

// If you don't want to override the data but to merge them
// The merge is recursive and works with Object and Array.
```

The third argument turns off the override and merges instead. The merge is recursive and works with objects and arrays, so merging an object into a stored object adds the keys rather than replacing the value, and nested objects are merged key by key.

The one rule to remember is that you cannot merge primitives. If you try to merge a number into a path that holds a number, the existing value is overridden instead, and the README says so directly: you cannot merge primitives, and the data will be overridden.

The default of overriding also means a careless push can silently replace a whole subtree. The merge flag exists for the case where you want to add to what is there, and the difference between the two behaviours is the difference between an update and a reset.

## Reading, defaults and the error that tells you where it stopped

Reading is symmetric with writing. You ask for the root, you ask for a path, and you get the value or an error.

```javascript
// Get the data from the root
var data = await db.getData("/");

// Or from a particular DataPath
var data = await db.getData("/test1");

// If you try to get some data from a DataPath that doesn't exist
// You'll get an Error
try {
  var data = await db.getData("/test1/test/dont/work");
} catch (error) {
  // The error will tell you where the DataPath stopped. In this case test1
  // Since /test1/test does't exist.
  console.error(error);
}
```

The error message is the useful part. It tells you where the DataPath stopped, so in the example the failure is at test1 because the path continues with a segment that does not exist. A generic exception would leave you guessing which part of the path was wrong.

When you would rather not write a try and catch for a path that may legitimately be absent, there is a default variant. The README describes it as returning the default value when the path does not lead to data, and the data otherwise.

Two more methods complete the lifecycle. The save method writes the file, which is what you need when you have turned off saving on each push. And the reload method re-reads the file, which the README describes for the case where something outside your process changed the database file and you want to pick that up.

That reload method is the escape hatch for the concurrency problem. Two processes writing the same JSON file will overwrite each other, and reload only helps you see the other version, not merge with it.

## Arrays, indexes and the append syntax

Arrays are addressed inside the path syntax with an index, and the array section of the README shows the three forms that matter.

```typescript
import { JsonDB, Config } from "node-json-db";

const db = new JsonDB(new Config("myDataBase", true, false, "/"));

interface FooBar {
  Hello: string;
  World: number;
}
const object = { Hello: "World", World: 5 } as FooBar;

await db.push("/test", object);

// Will be typed as FooBar in your IDE
const result = await db.getObject<FooBar>("/test");
```

Pushing to a path with an explicit index creates the array if it does not exist, and the third argument here is the same flag used for merge elsewhere, passed as true to create the hierarchy. Reading a property of an object stored inside an array works by putting the index in the path.

Deleting an index removes the object at that position. The README warns that this does not delete the array even when it ends up empty, so the array itself remains in the document.

The append form is the second one, a path ending with empty brackets. It sets the next index with the given value, and it can be combined with a property name to set a field of the next object rather than the object itself.

So the array story is complete enough for the common cases: create, read a member, append, delete an entry. What it is not is a query language over arrays, and there is no way to filter or sort them, which is the point at which a real database starts to look like the better answer.

## TypeScript support, typing and how the package is put together

The library ships its own types, and the README records the history in two steps.

As of version 0.8.0, TypeScript types are included in the package, so the separate types package is no longer required. As of version 1.0.0, JsonDB is no longer exported as a default, and the README explains the reason: the change follows the right way to import a module, which is the named import form.

```typescript
const db = new JsonDB(new Config("myDataBase", true, false, "/"));

// This will create an array 'myarray' with the object '{obj:'test'}' at index 0
await db.push(
  "/arraytest/myarray[0]",
  {
    obj: "test",
  },
  true,
);

// You can retrieve a property of an object included in an array
// testString = 'test';
var testString = await db.getData("/arraytest/myarray[0]/obj");
```

The typing story is the getObject generic. With an interface declared for your own shape, getObject with a type argument returns that type, so the result is typed in your editor rather than as an untyped object.

The package metadata shows how the project is built and released. The build is a plain TypeScript compile, the main entry points at the compiled JsonDB file with a sibling declaration file, and there is an imports map that resolves a path prefix to the source for types and to the compiled output otherwise. Tests run under Jest with coverage, the documentation is generated with typedoc using the expand entry point strategy, and the releases are handled by semantic-release with the changelog, npm and GitHub plugins, publishing to npm with provenance enabled.

The repository itself holds the source, the test directory the README points to for more usage detail, a jest configuration, a Husky setup, a changelog and a FOSSA configuration.

On versions, the tags are v2.6.0 from 2026-03-10, v2.5.0 from 2026-02-16 and v2.4.2 from 2025-11-07, and the last push was on 2026-10-19. The patch releases are frequent, and the semantic release setup means the version number reflects the change type rather than a manual decision.

## Conclusion

node-json-db is the right dependency for a script, a bot, a build tool or a side project that needs to persist a small amount of structured data and does not want a database server, because the whole model is a JSON file and a path, and the API is small enough to read in one sitting. It is the wrong choice for concurrent writers, for data that grows without bound, and for anything where a half written file would be a real loss, since the library rewrites the whole document. Verify first that your access pattern fits the save on push or manual save model you choose, that the human readable output setting does what you need for diffing, and that the version you install is 2.x, because 2.0 changed every method to asynchronous and forced the Config object.

## FAQ

### How does node-json-db store and address data?

It stores JavaScript objects directly in a JSON file and addresses them with a DataPath that follows the same principle as XPath, so a nested value is reached by a path such as the test, data1 and array segments in turn.

### What changed in node-json-db 2.0?

Two things, according to the README. You are now forced to use the Config object to set up the database, and every method is asynchronous, because the library reads the file on demand and may write on each push depending on configuration.

### How do I configure the file and the saving behaviour?

The Config object takes the database filename, whether to save after each push, whether to write human readable output, the path separator which defaults to a slash, and whether to use synchronous writes.

### How do I append to an array in node-json-db?

Push to a path ending with empty brackets, which sets the next index with the value you supply, and the same form works with a property name to set a field on the next object.

## Sources

- [Belphemur/node-json-db on GitHub](https://github.com/Belphemur/node-json-db)
- [Issues](https://github.com/Belphemur/node-json-db/issues)
- [License: MIT](https://github.com/Belphemur/node-json-db/blob/develop/LICENSE)
- [README](https://github.com/Belphemur/node-json-db/blob/develop/README.md)
- [Releases](https://github.com/Belphemur/node-json-db/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/belphemur-node-json-db
