npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

storage-to-json

v2.0.0

Published

Lightweight JSON-backed persistent storage for Node.js with nested observation, memory mode, backups, transactions, and utility helpers.

Readme

Storage-to-json

Small JSON-backed key/value storage for Node.js with automatic persistence, in-memory mode, nested mutation tracking, backups, transactions, path helpers, and compatibility helpers.

Installation

npm install storage-to-json

Quick start

const Storage = require('storage-to-json');

const Store = new Storage('testeroni');

Store.clear();

Store.set('MyName', { name: 'Gerkiz' });

console.log(Store.get('MyName'));
// { name: 'Gerkiz' }

console.log(Store.get('MyName').name);
// Gerkiz

Store.replace('MyName', 'FirstName');

console.log(Store.has('MyName'));
// false

console.log(Store.get('FirstName'));
// { name: 'Gerkiz' }

Store.ensure('settings', {
	enabled: true,
	maxPlayers: 32
});

Store.setPath('settings.discord.enabled', true);

console.log(Store.getPath('settings.discord.enabled'));
// true

Store.increment('joins');

Store.push('players', 'Gerkiz', 'Alice');

Store.each((key, value) => {
	console.log(key, value);
});

Store.backup();

Store.dispose();

Creating a storage

Persistent storage

Provide a name to create or open a JSON-backed datastore:

const Storage = require('storage-to-json');

const Store = new Storage('datastore');

This creates:

storage/datastore.json

The .json extension is optional:

new Storage('datastore');
new Storage('datastore.json');

Both use the same filename format.

Changes made through the storage object or its nested proxy are automatically written to disk.

Store.set('settings', {
	discord: {
		enabled: false
	}
});

const data = Store.proxy();

data.settings.discord.enabled = true;

The nested mutation is persisted automatically.

Memory storage

If no filename is provided, storage is kept in memory:

const Store = new Storage();

Memory storage is shared between memory-mode instances by default:

const A = new Storage();
const B = new Storage();

A.set('value', 123);

console.log(B.get('value'));
// 123

For private per-instance memory storage:

const Store = new Storage(null, {
	sharedMemory: false
});

Constructor options

const Store = new Storage('datastore', {
	directory: 'storage',
	backupDirectory: 'storage/backup',
	pretty: true,
	logger: true,
	sharedMemory: true
});

Available options:

| Option | Default | Description | | --- | --- | --- | | directory | 'storage' | Directory containing persistent JSON files. | | backupDirectory | <directory>/backup | Directory containing backups. | | pretty | true | Pretty-print JSON using tabs. | | logger | true | Enable Storage console logging. | | sharedMemory | true | Share memory-mode data between instances. |

sharedMemory only affects memory-mode stores.


Basic API

set(key, value)

Set or replace a top-level value.

Store.set('name', 'Gerkiz');

Store.set('user', {
	name: 'Gerkiz',
	admin: true
});

Falsy values are supported correctly:

Store.set('enabled', false);
Store.set('count', 0);
Store.set('text', '');
Store.set('nothing', null);

console.log(Store.has('enabled'));
// true

console.log(Store.get('enabled'));
// false

set() returns the Storage instance, allowing chaining:

Store
	.set('one', 1)
	.set('two', 2)
	.set('three', 3);

set(key, subkey, value)

Set a nested value one level below a key:

Store.set('settings', 'enabled', false);
Store.set('settings', 'maxPlayers', 32);

console.log(Store.get('settings'));
// {
//   enabled: false,
//   maxPlayers: 32
// }

set(object)

Set multiple top-level values:

Store.set({
	name: 'Gerkiz',
	enabled: true,
	count: 10
});

setMany(object)

Alias for setting multiple values:

Store.setMany({
	a: 1,
	b: 2,
	c: 3
});

get(key, defaultValue = false)

Get a top-level value.

Store.set('name', 'Gerkiz');

console.log(Store.get('name'));
// Gerkiz

Missing keys return false by default for backward compatibility:

console.log(Store.get('missing'));
// false

Provide a custom fallback:

console.log(Store.get('missing', 'fallback'));
// fallback

Falsy stored values are not confused with missing values:

Store.set('enabled', false);

console.log(Store.get('enabled', true));
// false

has(key)

Returns whether a top-level key exists.

Store.set('enabled', false);

console.log(Store.has('enabled'));
// true

console.log(Store.has('missing'));
// false

validate(key)

Backward-compatible alias for has():

Store.validate('enabled');

ensure(key, defaultValue)

Ensure a key exists.

If missing, defaultValue is stored and returned.

const settings = Store.ensure('settings', {
	enabled: true,
	maxPlayers: 32
});

Existing values are preserved:

Store.set('count', 10);

console.log(Store.ensure('count', 0));
// 10

Falsy defaults are supported:

Store.ensure('enabled', false);
Store.ensure('count', 0);
Store.ensure('text', '');
Store.ensure('value', null);

update(key, patch)

Shallow-merge an existing plain object.

Store.set('user', {
	name: 'Gerkiz',
	admin: false
});

Store.update('user', {
	admin: true,
	online: true
});

console.log(Store.get('user'));
// {
//   name: 'Gerkiz',
//   admin: true,
//   online: true
// }

If either the existing value or patch is not a plain object, the value is replaced:

Store.set('count', 1);
Store.update('count', 5);

console.log(Store.get('count'));
// 5

Returns false if the key does not exist.


Removing and renaming

remove(key)

Remove a top-level key.

Store.set('name', 'Gerkiz');

Store.remove('name');

console.log(Store.has('name'));
// false

Unlike older versions, the key is actually deleted instead of being set to null.

Returns:

true  - key removed
false - key did not exist

remove(key, subkey)

Remove a nested property one level below a key:

Store.set('settings', {
	enabled: true,
	secret: 'abc'
});

Store.remove('settings', 'secret');

delete(key, subkey?)

Alias for remove():

Store.delete('name');
Store.delete('settings', 'secret');

replace(sourceKey, replacementKey)

Rename a top-level key:

Store.set('MyName', {
	name: 'Gerkiz'
});

Store.replace('MyName', 'FirstName');

console.log(Store.has('MyName'));
// false

console.log(Store.get('FirstName'));
// { name: 'Gerkiz' }

Returns false when:

  • source key does not exist
  • destination key already exists

Otherwise returns true.

rename(sourceKey, replacementKey)

Alias for replace():

Store.rename('oldName', 'newName');

Reading the whole storage

storage

Live storage getter:

const data = Store.storage;

This is the live observed storage object.

Mutations are persisted:

Store.storage.enabled = true;
Store.storage.settings = {};

Store.storage.settings.discord = true;

getAll()

Returns the live storage object:

const data = Store.getAll();

get_storage()

Backward-compatible alias:

const data = Store.get_storage();

proxy()

Returns the same live observed object:

const data = Store.proxy();

data.user = {
	name: 'Gerkiz'
};

data.user.online = true;

Nested changes are observed and persisted automatically.

snapshot()

Returns a detached copy:

const copy = Store.snapshot();

copy.enabled = false;

Changing copy does not modify the storage.

toJSON()

Returns a detached copy suitable for serialization:

const json = JSON.stringify(Store);

Storage information

size

Number of top-level keys:

console.log(Store.size);

isEmpty

Returns true when no top-level keys exist:

if (Store.isEmpty) {
	console.log('Storage is empty');
}

keys()

const keys = Store.keys();

Equivalent to:

Object.keys(Store.storage);

values()

const values = Store.values();

entries()

const entries = Store.entries();

for (const [key, value] of entries) {
	console.log(key, value);
}

Iteration and searching

each(callback)

Iterate over all top-level entries.

Callback order is:

callback(key, value)

Example:

Store.each((key, value) => {
	console.log(key, value);
});

eachAsync(callback)

Await an async callback sequentially for every entry:

await Store.eachAsync(async (key, value) => {
	await doSomething(key, value);
});

find(predicate, defaultValue)

Find the first matching value:

Store.setMany({
	one: {
		name: 'Alice',
		admin: false
	},
	two: {
		name: 'Bob',
		admin: true
	}
});

const admin = Store.find(user => user.admin);

console.log(admin);
// { name: 'Bob', admin: true }

Optional fallback:

const user = Store.find(
	value => value.name === 'Nobody',
	null
);

firstOrDefault(predicate, defaultValue = null)

Backward-compatible helper:

const admin = Store.firstOrDefault(
	value => value.admin === true
);

Returns null by default when nothing matches.


filter(predicate)

Returns matching values:

const admins = Store.filter((value, key) => {
	return value.admin === true;
});

filterEntries(predicate)

Returns matching [key, value] pairs:

const admins = Store.filterEntries((value, key) => {
	return value.admin === true;
});

for (const [key, value] of admins) {
	console.log(key, value);
}

Nested path API

Dot paths make deeply nested storage easier to work with.

setPath(path, value)

Store.setPath('settings.discord.enabled', true);

Store.setPath('settings.discord.channel', 'general');

Missing plain objects are created automatically.

Array paths are also supported:

Store.setPath(
	['settings', 'discord', 'enabled'],
	true
);

getPath(path, defaultValue = false)

const enabled = Store.getPath(
	'settings.discord.enabled'
);

Custom fallback:

const port = Store.getPath(
	'server.port',
	3000
);

hasPath(path)

if (Store.hasPath('settings.discord.enabled')) {
	console.log('Discord setting exists');
}

removePath(path)

Store.removePath('settings.discord.enabled');

Returns true when removed, otherwise false.


Numeric helpers

increment(key, amount = 1)

Store.increment('joins');

Store.increment('score', 10);

If the key is missing, it starts at 0.

Store.increment('joins');

console.log(Store.get('joins'));
// 1

Returns the new value.

decrement(key, amount = 1)

Store.decrement('lives');
Store.decrement('balance', 10);

Returns the new value.


Boolean helpers

toggle(key, defaultValue = false)

Toggle a boolean:

Store.set('enabled', false);

Store.toggle('enabled');

console.log(Store.get('enabled'));
// true

If the key is missing:

Store.toggle('enabled');

The default value is first created and then toggled.

For example:

Store.toggle('enabled', false);
// true

Returns the new boolean value.


Array helpers

push(key, ...values)

Create or append to an array:

Store.push('players', 'Alice');
Store.push('players', 'Bob', 'Charlie');

console.log(Store.get('players'));
// ['Alice', 'Bob', 'Charlie']

Returns the new array length.

If the key exists and is not an array, a TypeError is thrown.

pop(key)

Remove and return the final array item:

const player = Store.pop('players');

Returns undefined if the key does not exist.


Transactions

transaction(callback)

Run multiple changes against a detached draft and commit them together.

This is useful when many individual proxy mutations would otherwise trigger many disk writes.

Store.transaction(data => {
	data.counter = (data.counter || 0) + 1;

	data.settings ??= {};
	data.settings.enabled = true;

	data.players ??= [];
	data.players.push('Gerkiz');
});

The callback receives a detached copy of the current storage.

When the callback finishes, the entire draft replaces the current storage and is persisted once.

A transaction callback must be synchronous:

Store.transaction(async data => {
	// Not supported.
});

Use normal async work before the transaction instead:

const player = await getPlayer();

Store.transaction(data => {
	data.player = player;
});

The return value from the callback is returned by transaction():

const result = Store.transaction(data => {
	data.counter = 10;
	return data.counter;
});

console.log(result);
// 10

Persistence

Persistent stores are normally saved automatically whenever the observed data changes.

save()

Force the current state to disk:

Store.save();

Returns false in memory mode.

saveAsync()

Asynchronously force the current state to disk:

await Store.saveAsync();

Unlike most compatibility *Async() helpers, this method uses asynchronous filesystem operations.

reload()

Reload the persistent JSON file from disk:

Store.reload();

Useful if another process or tool modifies the file.

In memory mode, the current memory object is returned.


Backups

backup(sourceFileName = currentStore)

Back up the current datastore:

const backupPath = Store.backup();

Example backup:

storage/backup/datastore.json.bak.2026-08-09T10-00-00-000Z

You can also back up another JSON datastore in the same storage directory:

Store.backup('other-store');

Returns:

destination path - backup created
false            - memory mode or source file missing

backupAsync(sourceFileName = currentStore)

Async filesystem version:

const backupPath = await Store.backupAsync();

listBackups(sourceFileName = currentStore)

List backups newest first:

const backups = Store.listBackups();

for (const backup of backups) {
	console.log(backup);
}

List backups for another store:

const backups = Store.listBackups('other-store');

Memory mode returns an empty array.


Clearing storage

clear()

Delete all keys:

Store.clear();

Works in both persistent and memory mode.

Persistent mode writes the empty storage to disk.

Returns the Storage instance:

Store
	.clear()
	.set('ready', true);

Logging

Logging is enabled by default.

logger(value)

Store.logger('Hello');
Store.logger({ hello: 'world' });

Output is prefixed with:

StorageLog:

enableLogger(enabled = true)

Store.enableLogger(true);
Store.enableLogger(false);

Returns the current logger state.

disableLogger()

Shortcut:

Store.disableLogger();

Equivalent to:

Store.enableLogger(false);

Logging can also be disabled at construction:

const Store = new Storage('datastore', {
	logger: false
});

Async compatibility API

The original API exposed several *Async() methods.

They remain available for backward compatibility:

await Store.getAsync('key');
await Store.setAsync('key', 'value');

await Store.hasAsync('key');
await Store.validateAsync('key');

await Store.get_storageAsync();

await Store.ensureAsync('key', 'default');

await Store.removeAsync('key');
await Store.deleteAsync('key');

await Store.replaceAsync('old', 'new');

await Store.eachAsync(async (key, value) => {
	await doSomething(key, value);
});

await Store.backupAsync();
await Store.saveAsync();

Most storage operations are in-memory operations backed by an automatically persisted proxy.

Because of that, methods such as getAsync(), setAsync(), hasAsync(), ensureAsync(), and removeAsync() are compatibility wrappers rather than separate asynchronous storage engines.

saveAsync() and backupAsync() use asynchronous filesystem operations.


Proxy behavior

Persistent storage is observed recursively.

This means direct nested mutations work:

Store.set('user', {
	name: 'Gerkiz',
	stats: {
		level: 1
	}
});

const data = Store.proxy();

data.user.name = 'Alice';
data.user.stats.level++;

data.user.roles = [];
data.user.roles.push('admin');

delete data.user.stats.level;

These changes are automatically persisted.

null, false, 0, '', and undefined are treated as values by the observer rather than being interpreted as delete operations.

For persistent JSON storage, prefer JSON-serializable values.

Supported JSON data includes:

objects
arrays
strings
numbers
booleans
null

Values such as functions, symbols, Map, Set, WeakMap, and WeakSet are not suitable for reliable JSON persistence.

undefined can exist in the live JavaScript object, but JSON itself cannot represent it. After saving and reloading, undefined properties will not be preserved.


Examples

Settings

const Storage = require('storage-to-json');

const Store = new Storage('settings');

Store.ensure('server', {
	name: 'My Server',
	enabled: true,
	maxPlayers: 32
});

Store.setPath('server.discord.enabled', true);
Store.setPath('server.discord.channel', 'general');

console.log(
	Store.getPath('server.discord.enabled')
);

Counters

Store.increment('connections');
Store.increment('connections');

console.log(Store.get('connections'));
// 2

Store.decrement('connections');

console.log(Store.get('connections'));
// 1

Player list

Store.push('players', {
	name: 'Alice',
	online: true
});

Store.push('players', {
	name: 'Bob',
	online: false
});

console.log(Store.get('players'));

Searching objects

Store.setMany({
	user1: {
	name: 'Alice',
	admin: true
	},
	user2: {
	name: 'Bob',
	admin: false
	},
	user3: {
	name: 'Charlie',
	admin: true
	}
});

const firstAdmin = Store.find(
	user => user.admin
);

const admins = Store.filter(
	user => user.admin
);

console.log(firstAdmin);
console.log(admins);

Batch update with transaction

Store.transaction(data => {
	data.server ??= {};
	data.server.online = true;
	data.server.startedAt = Date.now();

	data.statistics ??= {};
	data.statistics.starts =
		(data.statistics.starts || 0) + 1;
});

Full API overview

Properties

Store.storage
Store.size
Store.isEmpty

Read

Store.get(key, defaultValue)
Store.getAll()
Store.get_storage()
Store.proxy()
Store.snapshot()
Store.toJSON()

Store.has(key)
Store.validate(key)

Store.keys()
Store.values()
Store.entries()

Store.find(predicate, defaultValue)
Store.firstOrDefault(predicate, defaultValue)
Store.filter(predicate)
Store.filterEntries(predicate)

Write

Store.set(key, value)
Store.set(key, subkey, value)
Store.set(object)
Store.setMany(object)

Store.update(key, patch)
Store.ensure(key, defaultValue)

Store.replace(sourceKey, replacementKey)
Store.rename(sourceKey, replacementKey)

Remove

Store.remove(key)
Store.remove(key, subkey)

Store.delete(key)
Store.delete(key, subkey)

Store.clear()

Paths

Store.getPath(path, defaultValue)
Store.hasPath(path)
Store.setPath(path, value)
Store.removePath(path)

Numbers and booleans

Store.increment(key, amount)
Store.decrement(key, amount)
Store.toggle(key, defaultValue)

Arrays

Store.push(key, ...values)
Store.pop(key)

Iteration

Store.each(callback)
Store.eachAsync(callback)

Transactions and persistence

Store.transaction(callback)

Store.save()
Store.saveAsync()
Store.reload()

Store.backup(sourceFileName)
Store.backupAsync(sourceFileName)
Store.listBackups(sourceFileName)

Logging

Store.logger(value)
Store.enableLogger(enabled)
Store.disableLogger()

Compatibility async methods

Store.getAsync(key, defaultValue)
Store.setAsync(...args)

Store.get_storageAsync()

Store.hasAsync(key)
Store.validateAsync(key)

Store.ensureAsync(key, defaultValue)

Store.removeAsync(key, subkey)
Store.deleteAsync(key, subkey)

Store.replaceAsync(sourceKey, replacementKey)

Cleanup

Store.dispose()

Dispose

Call dispose() when you are finished with an instance, especially when creating and destroying stores dynamically:

Store.dispose();

After disposal, the instance should no longer be used.


Notes

  • Persistent storage is automatically created when missing.
  • Persistent root data must be a JSON object.
  • Arrays are supported as values, but the root datastore itself is an object.
  • Persistent mutations are automatically written to disk.
  • Writes use a temporary file followed by rename to reduce the chance of leaving a partially written JSON file.
  • Memory mode does not write files.
  • Memory mode is shared by default unless sharedMemory: false is used.
  • remove() and replace() now genuinely delete old keys instead of leaving them set to null.
  • Missing keys return false from get() by default for backward compatibility.
  • Use snapshot() when you need a detached copy that should not automatically persist changes.
  • Use transaction() when performing many related mutations that should be committed with one root replacement.