๐๏ธ Day 3 โ Persist Data With Files
14 min readยทJan 1, 2026
Welcome to Day 3.
In yesterday's lesson, you implemented the add and list commands and made them behave correctly with valid and invalid input.
In today's lesson, you'll make your notes persistent by saving them to disk and loading them back.
You'll learn the minimal filesystem workflow behind many backend projects: read state, update it, write it back โ plus the common failure cases around missing or empty files.
By the end of this lesson, notes you add will still be there after you close and reopen your terminal.
Read from and write to a JSON file
Within the src directory, let's create a new directory named storage, and within it, a new file named json_fs.js.
~/projects/
โโ lb_notes/
โโ bin/
โโ package.json
โโ src/
โโ cli/
โโ storage/
โโ json_fs.js
Implement the readJSON() function
Within the json_fs.js file, let's export the following function used to read the content of a file and return its content in the form of a JSON object, or otherwise return null if the file doesn't exist or it is zero-lengthed.
import fs from 'node:fs';
export function readJSON(filePath) {
if (!fs.existsSync(filePath)) {
return null;
}
let data = fs.readFileSync(filePath, 'utf8');
data = data?.trim();
if (!data.length) {
return null;
}
return JSON.parse(data);
}
Where:
filePathis a string that represents the path to the source file.
Implement the writeJSON() function
Within the json_fs.js file, let's export the following function used to write a JSON object as a string into a file, and automatically create the intermediary directories of its path if they don't exist.
import fs from 'node:fs';
import path from 'node:path';
export function readJSON(filePath) {
// ...
}
export function writeJSON(filePath, data) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
}
Where:
filePathis a string that represents the path to the destination file.datais a JSON object that represents a list of notes.
Update the add command
Let's now update the previous implementation of the add() function so that it either:
- Adds the note to the list and saves the list into the specified file on the disk.
- Throws an error if the note is empty or the content of the file is invalid.
import { readJSON, writeJSON } from '../../storage/json_fs.js';
export default function add(filePath, note) {
let data = readJSON(filePath);
note = note?.trim();
if (!data) {
data = { notes: [] };
}
else if (!data?.notes || !Array.isArray(data?.notes)) {
throw new Error('Invalid JSON file');
}
else if (!note.length) {
throw new Error('Invalid note');
}
data.notes.push(note);
writeJSON(filePath, data);
console.log('Note saved!');
}
Where:
filePathis a string that represents the path to the JSON file containing the notes.noteis a string that represents a note.
Update the list command
Let's update the previous implementation of the list() function so that it either:
- Outputs an empty array if the specified file is empty or the list of notes if it's not.
- Throws an error if the content of the file is invalid.
import { readJSON } from '../../storage/json_fs.js';
export default function list(filePath) {
const data = readJSON(filePath);
if (!data) {
console.log([]);
}
else if (!data?.notes || !Array.isArray(data?.notes)) {
throw new Error('Invalid JSON file');
}
else {
console.log(data.notes);
}
}
Where:
filePathis a string that represents the path to the JSON file containing the notes.
Update the CLI router
Let's update the CLI router by declaring a NOTES_FILE_PATH constant that contains the absolute path to the JSON file used to store the notes, in this case data/notes.json, and use this constant as argument when executing the add() and list() functions.
import path from 'node:path';
import parseArgs from './parse_args.js';
import help from './commands/help.js';
import add from './commands/add.js';
import list from './commands/list.js';
export default function run(args) {
try {
const { command, value } = parseArgs(args);
const NOTES_FILE_PATH = path.resolve('data/notes.json');
if (command === 'help') {
help();
}
else if (command === 'add') {
add(NOTES_FILE_PATH, value);
}
else if (command === 'list') {
list(NOTES_FILE_PATH);
}
} catch(error) {
console.error(`lb_notes: error: ${error.message}`);
return 1;
}
return 0;
}
Test the CLI
When running the lb_notes command with the add command and valid strings as argument, it should output "Note saved!" each time.
~/projects/lb_notes$ lb_notes add "Get a carton of eggs"
Note saved!
~/projects/lb_notes$ lb_notes add "Get a loaf of bread"
Note saved!
When running the lb_notes command with the list command, it should output a list of notes.
~/projects/lb_notes$ lb_notes list
[ 'Get a carton of eggs', 'Get a loaf of bread' ]
Finally, when outputting the content of the data/notes.json file, it should produce this object.
~/projects/lb_notes$ cat data/notes.json
{
"notes": [
"Get a carton of eggs",
"Get a loaf of bread"
]
}