🚀AdvancedNode.js Practice Projects
Project: Notes App using fs
Build a command-line notes manager that persists data to a JSON file using fs/promises
Overview
This project brings together everything you have learned about the fs module, the path module, environment variables, and the process object into a real, working command-line application. You will build a notes manager — a CLI tool that lets you add, list, read, delete, and search plain-text notes that persist across sessions in a JSON file.
The notes app is a perfect first Node.js project because it touches every fundamental I/O pattern without requiring a web server or external libraries. You will practice reading and writing JSON files atomically, handling missing-file edge cases gracefully, parsing command-line arguments from process.argv, formatting output for the terminal, and structuring a Node.js script as a set of async functions with proper error handling.
You will also confront a practical architectural decision: where to store the notes file. Storing it relative to __dirname makes the app portable but clutters your source directory. Storing it in os.homedir() (like ~/.notes.json) mirrors what real CLI tools do. You will implement the ~/.notes.json approach so users see the same notes regardless of which directory they run the command from.
By the end of this project you will have a fully functional CLI tool you can actually use, and a solid mental model for any Node.js script that needs to persist structured data to disk.
Key Points
- •Store notes in ~/.notes.json using os.homedir() so the data persists across working directories
- •Use a read-parse-modify-write pattern for every mutation: read the file, update in memory, write it back atomically
- •Handle ENOENT (file not found) on first run by returning an empty array instead of crashing
- •Parse command-line arguments from process.argv.slice(2) and dispatch to command functions
- •Use process.exit(1) with a descriptive error message for invalid input and missing required args
- •Format terminal output with ANSI colour codes or simple Unicode box characters for readability
Examples
Final app — command-line interface
# The finished notes app supports these commands:
node notes.js add "Buy groceries"
# ✅ Note added (id: 3)
node notes.js list
# 1. [2026-06-15] Buy groceries
# 2. [2026-06-14] Call dentist
# 3. [2026-06-14] Read Node.js docs
node notes.js read 1
# ID: 1
# Created: 2026-06-15T10:30:00.000Z
# Content: Buy groceries
node notes.js delete 2
# ✅ Note 2 deleted
node notes.js search "groceries"
# 1. [2026-06-15] Buy groceriesCore data model — reading and writing the JSON store
const fs = require('fs/promises');
const os = require('os');
const path = require('path');
const STORE = path.join(os.homedir(), '.notes.json');
async function readNotes() {
try {
const raw = await fs.readFile(STORE, 'utf8');
return JSON.parse(raw);
} catch (err) {
if (err.code === 'ENOENT') return []; // first run
throw err;
}
}
async function writeNotes(notes) {
await fs.writeFile(STORE, JSON.stringify(notes, null, 2), 'utf8');
}Adding a note with auto-increment ID
async function addNote(content) {
if (!content?.trim()) {
console.error('Error: note content cannot be empty');
process.exit(1);
}
const notes = await readNotes();
const id = notes.length > 0 ? Math.max(...notes.map(n => n.id)) + 1 : 1;
notes.push({ id, content: content.trim(), createdAt: new Date().toISOString() });
await writeNotes(notes);
console.log(`✅ Note added (id: ${id})`);
}CLI dispatcher — routing commands to functions
const [,, command, ...rest] = process.argv;
const COMMANDS = { add, list, read, delete: deleteNote, search };
async function main() {
const fn = COMMANDS[command];
if (!fn) {
console.error(`Unknown command: ${command}`);
console.error('Commands: add, list, read, delete, search');
process.exit(1);
}
try {
await fn(...rest);
} catch (err) {
console.error('Error:', err.message);
process.exit(1);
}
}
main();Common Mistakes
- Writing back to the file in-place without reading first — always read→modify→write to avoid data loss
- Storing the file relative to process.cwd() — this means notes disappear when you run the script from a different directory
- Forgetting to parseInt() the ID argument from process.argv — all argv values are strings, so id === 1 will always be false if you compare to a number without parsing
- Not handling concurrent writes — if two instances of the script run simultaneously they can both read the old data and overwrite each other; for a personal CLI tool this is acceptable, but worth noting
Note
Add a shebang line `#!/usr/bin/env node` at the top of notes.js and run `chmod +x notes.js` to make it executable without the `node` prefix. Then symlink it to /usr/local/bin/notes for system-wide access.
Exercises
Write Node.js code in your local terminal. Use the editor below to follow along, then mark each exercise as done once it works.
Loading editor…
Completed: 0 / 6 exercises
Knowledge Check
Question 1 of 4