Skip to main content

File System Module (fs)

The fs module is one of the most useful built-in modules in Node.js. It allows you to work with the file system on your computer: reading files, creating files, updating files, deleting files, and renaming files. To use it, you must first require it:
const fs = require('fs');

Synchronous vs Asynchronous

Most methods in the fs module have both synchronous and asynchronous versions.
  • Asynchronous methods take a callback function as the last argument. They are non-blocking.
  • Synchronous methods block the execution until the operation completes. They usually end with Sync.
Recommendation: Always use asynchronous methods in production to avoid blocking the event loop.

Reading Files

Asynchronous Read

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);
});
Note: If you don’t specify the encoding (‘utf8’), you will get a Buffer object instead of a string.

Synchronous Read

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

Writing Files

Asynchronous Write

fs.writeFile() replaces the file and content if it exists. If the file doesn’t exist, a new file, containing the specified content, will be created.
const content = 'Hello, this is new content!';

fs.writeFile('output.txt', content, err => {
  if (err) {
    console.error(err);
    return;
  }
  console.log('File written successfully');
});

Appending to Files

To add content to the end of a file without replacing it, use fs.appendFile().
fs.appendFile('output.txt', '\nThis is appended text.', err => {
  if (err) throw err;
  console.log('Content appended!');
});

Directories

Creating a Directory

if (!fs.existsSync('./new-folder')) {
  fs.mkdir('./new-folder', err => {
    if (err) throw err;
    console.log('Directory created');
  });
}

Reading a Directory

fs.readdir('./', (err, files) => {
  if (err) console.log(err);
  else console.log(files); // Returns an array of filenames
});

Removing Files and Directories

// Remove a file
fs.unlink('./output.txt', (err) => {
  if (err) throw err;
  console.log('File deleted');
});

// Remove an empty directory
fs.rmdir('./empty-folder', (err) => {
  if (err) throw err;
  console.log('Directory removed');
});

// Remove directory recursively (Node.js 14.14+)
fs.rm('./folder-with-files', { recursive: true, force: true }, (err) => {
  if (err) throw err;
  console.log('Directory removed recursively');
});

Promise-based API (fs/promises)

Modern Node.js provides a promise-based API that works beautifully with async/await.
const fs = require('fs/promises');
const path = require('path');

async function fileOperations() {
  try {
    // Read file
    const content = await fs.readFile('input.txt', 'utf8');
    console.log(content);

    // Write file
    await fs.writeFile('output.txt', 'New content');

    // Append to file
    await fs.appendFile('output.txt', '\nMore content');

    // Get file stats
    const stats = await fs.stat('output.txt');
    console.log('File size:', stats.size);
    console.log('Is file:', stats.isFile());
    console.log('Is directory:', stats.isDirectory());
    console.log('Created:', stats.birthtime);
    console.log('Modified:', stats.mtime);

    // Rename file
    await fs.rename('output.txt', 'renamed.txt');

    // Copy file
    await fs.copyFile('renamed.txt', 'backup.txt');

    // Create directory
    await fs.mkdir('./new-folder', { recursive: true });

    // Read directory
    const files = await fs.readdir('./');
    console.log('Files:', files);

  } catch (error) {
    console.error('Error:', error.message);
  }
}

fileOperations();
Always use fs/promises with async/await for cleaner, more maintainable code in modern Node.js applications.

Watching Files for Changes

const fs = require('fs');

// Watch a file for changes
fs.watch('config.json', (eventType, filename) => {
  console.log(`Event: ${eventType}, File: ${filename}`);
  if (eventType === 'change') {
    console.log('Config file was modified!');
    // Reload configuration
  }
});

// Watch a directory
fs.watch('./src', { recursive: true }, (eventType, filename) => {
  console.log(`${eventType}: ${filename}`);
});

Working with JSON Files

const fs = require('fs/promises');

// Read and parse JSON
async function readJSON(filepath) {
  const data = await fs.readFile(filepath, 'utf8');
  return JSON.parse(data);
}

// Write JSON with formatting
async function writeJSON(filepath, data) {
  const json = JSON.stringify(data, null, 2); // Pretty print with 2 spaces
  await fs.writeFile(filepath, json);
}

// Usage
async function main() {
  // Read existing data
  const config = await readJSON('./config.json');
  console.log('Current config:', config);

  // Modify and save
  config.updatedAt = new Date().toISOString();
  await writeJSON('./config.json', config);
}

Practical Example: File-based Logger

const fs = require('fs/promises');
const path = require('path');

class FileLogger {
  constructor(logDir = './logs') {
    this.logDir = logDir;
    this.init();
  }

  async init() {
    // Ensure log directory exists
    await fs.mkdir(this.logDir, { recursive: true });
  }

  getLogFileName() {
    const date = new Date().toISOString().split('T')[0];
    return path.join(this.logDir, `app-${date}.log`);
  }

  formatMessage(level, message) {
    const timestamp = new Date().toISOString();
    return `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
  }

  async log(level, message) {
    const logFile = this.getLogFileName();
    const formatted = this.formatMessage(level, message);
    await fs.appendFile(logFile, formatted);
  }

  async info(message) {
    await this.log('info', message);
  }

  async error(message) {
    await this.log('error', message);
  }

  async warn(message) {
    await this.log('warn', message);
  }

  // Get recent logs
  async getRecentLogs(lines = 50) {
    const logFile = this.getLogFileName();
    try {
      const content = await fs.readFile(logFile, 'utf8');
      const logLines = content.trim().split('\n');
      return logLines.slice(-lines);
    } catch (err) {
      return [];
    }
  }

  // Clean old logs
  async cleanOldLogs(daysToKeep = 30) {
    const files = await fs.readdir(this.logDir);
    const cutoffDate = new Date();
    cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);

    for (const file of files) {
      const filePath = path.join(this.logDir, file);
      const stats = await fs.stat(filePath);
      
      if (stats.mtime < cutoffDate) {
        await fs.unlink(filePath);
        console.log(`Deleted old log: ${file}`);
      }
    }
  }
}

module.exports = FileLogger;

Summary

  • Use asynchronous methods in production (avoid Sync methods)
  • Prefer fs/promises with async/await for modern code
  • Use fs.watch() to monitor file/directory changes
  • fs.stat() provides file metadata (size, dates, type)
  • Always handle errors when working with the file system
  • Create reusable utilities for common patterns (logging, JSON handling)

Removing a Directory

if (fs.existsSync('./new-folder')) {
  fs.rmdir('./new-folder', err => {
    if (err) console.log(err);
    else console.log('Directory removed');
  });
}

Deleting Files

if (fs.existsSync('output.txt')) {
  fs.unlink('output.txt', err => {
    if (err) throw err;
    console.log('File deleted');
  });
}

Promise-based API

Node.js also provides a Promise-based API for fs, which is cleaner to use with async/await.
const fs = require('fs/promises');

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

readFile();

Streams

For large files, reading the whole file into memory (like readFile does) is inefficient. Streams allow you to process data piece by piece.
const fs = require('fs');

const readStream = fs.createReadStream('largefile.txt', { encoding: 'utf8' });
const writeStream = fs.createWriteStream('copy.txt');

readStream.on('data', (chunk) => {
  console.log('--- NEW CHUNK ---');
  console.log(chunk);
  writeStream.write(chunk);
});

// Piping (easier way to copy)
// readStream.pipe(writeStream);

Summary

  • Use fs.readFile and fs.writeFile for simple file operations.
  • Prefer asynchronous methods or the Promise API (fs/promises) to avoid blocking.
  • Use Streams for handling large datasets efficiently.
  • Always handle errors (e.g., file not found, permission denied).