Skip to content

Instantly share code, notes, and snippets.

@mandiwise
Last active March 4, 2026 21:39
Show Gist options
  • Select an option

  • Save mandiwise/20c0925f3de2e757d67e919410026111 to your computer and use it in GitHub Desktop.

Select an option

Save mandiwise/20c0925f3de2e757d67e919410026111 to your computer and use it in GitHub Desktop.
Write data to JSON files as a simple persistence layer for Node.js
import jsonStore from "json-store";
const store = jsonStore();
const data = await store.list();
// Inspiration: https://github.com/alexkwolfe/json-fs-store
import fs from "fs/promises";
import path from "path";
import { v4 as uuidv4 } from "uuid";
export default function (dir = path.join(process.cwd(), "store")) {
async function ensureDir() {
await fs.mkdir(dir, { recursive: true });
}
async function readdirFull(dir) {
const files = await fs.readdir(dir);
return files.map(f => path.join(dir, f));
}
async function loadFile(filePath) {
try {
const code = await fs.readFile(filePath, "utf8");
return JSON.parse(code);
} catch (error) {
throw new Error(`Error loading/parsing ${filePath}: ${error.message}`);
}
}
async function atomicWriteFile(targetPath, data, encoding = "utf8") {
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`;
// Open explicitly so we can fsync
const handle = await fs.open(tmpPath, "w");
try {
await handle.writeFile(data, { encoding });
await handle.sync(); // fsync file contents
} finally {
await handle.close();
}
// Atomic replace
await fs.rename(tmpPath, targetPath);
}
async function getUniqueName(name) {
await ensureDir();
const files = await readdirFull(dir);
const jsonFiles = files.filter(f => f.endsWith(".json"));
const objects = await Promise.all(jsonFiles.map(loadFile));
const existingNames = new Set(objects.map(o => o.name).filter(Boolean));
if (!existingNames.has(name)) {
return name;
}
let nameBase = name;
let counter = 1;
const suffixRegex = /-(\d+)$/;
const match = name.match(suffixRegex);
if (match) {
counter = parseInt(match[1], 10);
nameBase = name.replace(suffixRegex, "");
}
while (existingNames.has(`${nameBase}-${counter}`)) {
counter++;
}
return `${nameBase}-${counter}`;
}
return {
dir,
// List all stored objects
async list() {
await ensureDir();
const files = await readdirFull(dir);
const jsonFiles = files.filter(f => f.endsWith(".json"));
const objects = await Promise.all(jsonFiles.map(loadFile));
return objects.sort((a, b) => (a.name || "").localeCompare(b.name || ""));
},
// Store an object in a file (atomic)
async add(obj, enforceUniqueName = false) {
await ensureDir();
let json;
obj.id = obj.id || uuidv4();
if (enforceUniqueName) {
const name = obj.name || "Untitled";
const uniqueName = await getUniqueName(name);
obj.name = uniqueName;
}
try {
json = JSON.stringify(obj, null, 2);
} catch (error) {
throw error;
}
const filePath = path.join(dir, `${obj.id}.json`);
await atomicWriteFile(filePath, json);
return json;
},
// Update a stored object
async update(obj, enforceUniqueName = false) {
await ensureDir();
const { id, name: newName } = obj;
const filePath = path.join(dir, `${id}.json`);
const existingFile = await loadFile(filePath);
if (!existingFile) {
throw new Error("File not found");
}
if (enforceUniqueName && existingFile.name !== newName) {
const uniqueName = await getUniqueName(newName);
if (newName !== uniqueName) {
throw new Error("Name already in use");
}
}
let json;
try {
json = JSON.stringify(obj, null, 2);
} catch (error) {
throw error;
}
await atomicWriteFile(filePath, json);
return json;
},
// Delete an object's file
async remove(id) {
await ensureDir();
const filePath = path.join(dir, `${id}.json`);
await fs.unlink(filePath);
},
// Load an object from file
async load(id) {
await ensureDir();
const filePath = path.join(dir, `${id}.json`);
return loadFile(filePath);
}
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment