Topic 22 of 56 · Full Stack Advanced

Topic 6 : File System

Lesson TL;DRTopic 6: File System 📖 7 min read · 🎯 intermediate · 🧭 Prerequisites: basicsstylescomponentstextinputsbuttonsscrollviewactivityindicatorimagesmodals, events Why this matters Up until now, your Node...
7 min read·intermediate·node-js · file-system · streams · directories

Topic 6: File System

📖 7 min read · 🎯 intermediate · 🧭 Prerequisites: basics-stylescomponents-text-inputs-buttons-scrollview-activity-indicator-images-modals, events

Why this matters

Up until now, your Node.js code has been storing everything in memory — variables, arrays, objects. The moment you restart the server, it's all gone. That's fine for learning, but no real application works that way. Users expect their data to stick around. That's where the file system comes in. Node.js ships with a built-in fs module — no installs, no third-party packages — that lets your code read, write, append, delete, and watch files and directories. By the end of this lesson, your app can actually save things that last.

What You'll Learn

  • Import and use Node.js's built-in fs module for file operations
  • Read, write, append, and delete files using both synchronous and asynchronous methods
  • Create, list, and remove directories programmatically
  • Watch files and directories for live changes with fs.watch
  • Use readable and writable streams for memory-efficient handling of large files

The Analogy

Think of the fs module as the city archive in Vizag's town hall. The archive clerk (Node.js) can hand you a document immediately while you wait at the counter — that's synchronous access, fast but it blocks the whole queue behind you. Alternatively, you can leave a request slip and come back when the clerk calls your name — that's asynchronous access, slower to start but the queue keeps moving. The archive holds individual files (scrolls) and folders of related scrolls (directories). For enormous manuscripts too big to carry in one trip, the clerk sends them page-by-page via a conveyor belt — that's a stream.

Chapter 1: Introduction to the fs Module

Node.js ships with the fs (file system) module in its standard library — no installation needed. It exposes methods for every common file operation and comes in two flavors for most of them: asynchronous (non-blocking, callback-based) and synchronous (blocking, returns a value directly).

Importing the module is one line:

const fs = require('fs');

That single require gives you access to every method covered in this lesson. All examples below assume this import is present at the top of the file.

graph TD
    A[fs module] --> B[Files]
    A --> C[Directories]
    A --> D[Streams]
    A --> E[Watchers]
    B --> B1[read / write / append / delete]
    C --> C1[mkdir / readdir / rmdir]
    D --> D1[createReadStream / createWriteStream]
    E --> E1[watch]

Chapter 2: Reading Files

The class's first task was pulling content out of files that already existed on disk.

Asynchronous File Reading — readFileAsync.js

The callback receives an err object first (Node convention), then the file data. The event loop is free to handle other work while the OS fetches the file.

const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log(data);
});

Synchronous File Reading — readFileSync.js

readFileSync returns the file contents directly. Execution pauses until the read completes, so wrap it in try/catch — if the file doesn't exist, it throws.

const fs = require('fs');

try {
    const data = fs.readFileSync('example.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error(err);
}

When to pick which: Use the async form in servers and anywhere throughput matters. Use the sync form in startup scripts, CLI tools, or configuration loading that must complete before anything else runs.

Chapter 3: Writing Files

writeFile / writeFileSync creates the file if it doesn't exist and overwrites it completely if it does.

Asynchronous File Writing — writeFileAsync.js

const fs = require('fs');

const content = 'Hello, Vizag!';

fs.writeFile('example.txt', content, 'utf8', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('File has been saved.');
});

Synchronous File Writing — writeFileSync.js

const fs = require('fs');

const content = 'Hello, Vizag!';

try {
    fs.writeFileSync('example.txt', content, 'utf8');
    console.log('File has been saved.');
} catch (err) {
    console.error(err);
}

Chapter 4: Updating Files (Appending)

When you need to add content without erasing what's already there, reach for appendFile / appendFileSync. They open the file in append mode, positioning the write cursor at the end.

Asynchronous File Appending — appendFileAsync.js

const fs = require('fs');

const additionalContent = '\nWelcome to Vizag!';

fs.appendFile('example.txt', additionalContent, 'utf8', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Content has been appended.');
});

Synchronous File Appending — appendFileSync.js

const fs = require('fs');

const additionalContent = '\nWelcome to Vizag!';

try {
    fs.appendFileSync('example.txt', additionalContent, 'utf8');
    console.log('Content has been appended.');
} catch (err) {
    console.error(err);
}

Chapter 5: Deleting Files

To remove a file from disk, use fs.unlink (async) or fs.unlinkSync (sync). The name comes from the Unix system call that removes a directory entry.

Asynchronous File Deletion — deleteFileAsync.js

const fs = require('fs');

fs.unlink('example.txt', (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('File has been deleted.');
});

Synchronous File Deletion — deleteFileSync.js

const fs = require('fs');

try {
    fs.unlinkSync('example.txt');
    console.log('File has been deleted.');
} catch (err) {
    console.error(err);
}

Chapter 6: Working with Directories

Files live inside directories, so the fs module also covers directory creation, listing, and removal.

Creating a Directory — createDirectory.js

The { recursive: true } option mirrors mkdir -p: it creates all intermediate directories and does not throw if the target already exists.

const fs = require('fs');

const dirPath = './newDir';

// Asynchronous
fs.mkdir(dirPath, { recursive: true }, (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory created.');
});

// Synchronous
try {
    fs.mkdirSync(dirPath, { recursive: true });
    console.log('Directory created.');
} catch (err) {
    console.error(err);
}

Reading a Directory — readDirectory.js

fs.readdir returns an array of entry names (not full paths) inside the given directory.

const fs = require('fs');

const dirPath = './';

fs.readdir(dirPath, (err, files) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory contents:', files);
});

Deleting a Directory — deleteDirectory.js

{ recursive: true } removes the directory and all its contents. Without it, the call fails on non-empty directories.

const fs = require('fs');

const dirPath = './newDir';

// Asynchronous
fs.rmdir(dirPath, { recursive: true }, (err) => {
    if (err) {
        console.error(err);
        return;
    }
    console.log('Directory deleted.');
});

// Synchronous
try {
    fs.rmdirSync(dirPath, { recursive: true });
    console.log('Directory deleted.');
} catch (err) {
    console.error(err);
}

Chapter 7: Watching Files

fs.watch registers a listener that fires whenever the OS detects a change (write, rename, delete) on a file or directory. It's the foundation of live-reload tooling and config hot-reloading.

watchFile.js

const fs = require('fs');

const filePath = 'example.txt';

fs.watch(filePath, (eventType, filename) => {
    if (filename) {
        console.log(`${filename} file changed with event type: ${eventType}`);
    } else {
        console.log('filename not provided');
    }
});

eventType will be either 'rename' (file created, deleted, or moved) or 'change' (file content modified). filename can be null on some platforms, so always guard with an if (filename) check.

Chapter 8: File Streams

Loading an entire large file into memory with readFile can exhaust RAM and slow your application to a crawl. Streams solve this by processing data in small chunks — the file never lives entirely in memory at once.

Reading with Streams — readStream.js

const fs = require('fs');

const readStream = fs.createReadStream('largeFile.txt', 'utf8');

readStream.on('data', (chunk) => {
    console.log('New chunk received:', chunk);
});

readStream.on('end', () => {
    console.log('File reading completed.');
});

readStream.on('error', (err) => {
    console.error('Error reading file:', err);
});

Writing with Streams — writeStream.js

const fs = require('fs');

const writeStream = fs.createWriteStream('largeFile.txt');

writeStream.write('This is the first line.\n');
writeStream.write('This is the second line.\n');
writeStream.end();

writeStream.on('finish', () => {
    console.log('File writing completed.');
});

writeStream.on('error', (err) => {
    console.error('Error writing file:', err);
});

Key stream events to know:

EventStream typeMeaning
dataReadableA new chunk of data is available
endReadableAll data has been consumed
finishWritableAll writes have been flushed to the OS
errorBothAn error occurred during the operation

🧪 Try It Yourself

Task: Build a simple log rotator that appends a timestamped entry to app.log, then reads the file back and prints its full contents to the console.

Success criterion: Running the script twice should show two timestamped lines in the console output — one from each run.

Starter snippet:

const fs = require('fs');

const logPath = 'app.log';
const entry = `[${new Date().toISOString()}] Server started\n`;

// Step 1: Append the log entry
fs.appendFile(logPath, entry, 'utf8', (err) => {
    if (err) {
        console.error('Failed to write log:', err);
        return;
    }

    // Step 2: Read and print the full log
    fs.readFile(logPath, 'utf8', (err, data) => {
        if (err) {
            console.error('Failed to read log:', err);
            return;
        }
        console.log('--- app.log contents ---');
        console.log(data);
    });
});

Run it with node solution.js. Run it a second time — you should see both timestamps printed.

🔍 Checkpoint Quiz

Q1. What is the key behavioral difference between fs.readFile and fs.readFileSync?

A) readFile only works with text; readFileSync works with any encoding
B) readFile is non-blocking and uses a callback; readFileSync blocks execution until the read completes
C) readFileSync requires a try/catch; readFile does not handle errors
D) There is no difference — both block the event loop

Q2. Given this code, what is printed to the console if example.txt does not exist?

const fs = require('fs');
try {
    const data = fs.readFileSync('example.txt', 'utf8');
    console.log(data);
} catch (err) {
    console.error(err);
}
console.log('done');

A) Nothing — the process crashes silently
B) The error is printed, then done is printed
C) done is printed, then the error is printed
D) Only done is printed

Q3. You have a 4 GB log file you need to process line-by-line. Which fs approach is most appropriate and why?

A) fs.readFileSync — simpler code
B) fs.readFile — non-blocking so it won't slow the server
C) fs.createReadStream — processes data in chunks without loading the whole file into memory
D) fs.appendFile — fastest read method for large files

Q4. The following watcher script is running. A user renames example.txt to example2.txt. What value does eventType hold inside the callback?

fs.watch('example.txt', (eventType, filename) => {
    console.log(eventType);
});

A) 'change'
B) 'delete'
C) 'rename'
D) 'unlink'

A1. B — readFile is asynchronous and delivers results via a callback, keeping the event loop free. readFileSync halts execution on that thread until the OS returns the data.

A2. B — readFileSync throws when the file is missing; the catch block handles it and logs the error. Execution then continues normally, printing done.

A3. C — fs.createReadStream emits chunks as the OS delivers them, keeping memory usage proportional to chunk size (not file size). readFile and readFileSync both load the entire file into a Buffer before your code can touch it — catastrophic for 4 GB.

A4. C — A rename triggers the 'rename' event type, not 'change'. The 'change' event fires when file content is modified in place.

🪞 Recap

  • The fs module is built into Node.js — require('fs') is all you need to start reading, writing, or managing files.
  • Every major operation has an asynchronous (callback-based, non-blocking) and a synchronous (blocking, returns directly) variant — prefer async in servers.
  • writeFile overwrites; appendFile adds to the end — choose based on whether you want to replace or accumulate.
  • fs.mkdir, fs.readdir, and fs.rmdir give you full control over directory trees; { recursive: true } is your friend for nested structures.
  • fs.watch lets your application react to file system changes in real time without polling.
  • For large files, fs.createReadStream and fs.createWriteStream chunk data through the event loop instead of holding it all in memory.

📚 Further Reading

Like this topic? It’s one of 56 in Full Stack Advanced.

Block your seat for ₹2,500 and join the next cohort.