🚀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 groceries

Core 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

Why should the notes JSON file be stored in os.homedir() rather than relative to process.cwd()?