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.
Maintainers
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-jsonQuick 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.jsonThe .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'));
// 123For 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'));
// falseset() 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'));
// GerkizMissing keys return false by default for backward compatibility:
console.log(Store.get('missing'));
// falseProvide a custom fallback:
console.log(Store.get('missing', 'fallback'));
// fallbackFalsy stored values are not confused with missing values:
Store.set('enabled', false);
console.log(Store.get('enabled', true));
// falsehas(key)
Returns whether a top-level key exists.
Store.set('enabled', false);
console.log(Store.has('enabled'));
// true
console.log(Store.has('missing'));
// falsevalidate(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));
// 10Falsy 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'));
// 5Returns 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'));
// falseUnlike older versions, the key is actually deleted instead of being set to null.
Returns:
true - key removed
false - key did not existremove(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'));
// 1Returns 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'));
// trueIf the key is missing:
Store.toggle('enabled');The default value is first created and then toggled.
For example:
Store.toggle('enabled', false);
// trueReturns 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);
// 10Persistence
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-000ZYou 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 missingbackupAsync(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
nullValues 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'));
// 1Player 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.isEmptyRead
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: falseis used. remove()andreplace()now genuinely delete old keys instead of leaving them set tonull.- Missing keys return
falsefromget()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.
