javascript-today

File System Operations

One of NodeJS’s most powerful features is direct access to the file system. The built-in fs module lets you read, write, delete, and manipulate files and directories.

The fs Module

const fs = require('fs');

The fs module has two styles of APIs:

  1. Synchronous (blocking) - ends with Sync
  2. Asynchronous (non-blocking) - callback or promise-based

Reading Files

Synchronous (Blocking)

const fs = require('fs');

// Read entire file
const data = fs.readFileSync('data.txt', 'utf8');
console.log(data);

// Without 'utf8', returns a Buffer (binary data)
const buffer = fs.readFileSync('data.txt');
console.log(buffer);  // <Buffer 48 65 6c 6c 6f>
console.log(buffer.toString());  // "Hello"

⚠️ Blocks execution until file is read - not recommended for servers.

Asynchronous (Callback)

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

console.log('This runs immediately!');

Asynchronous (Promises)

Modern and recommended approach:

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

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

readMyFile();

Or with .then():

fs.readFile('data.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

Writing Files

Write (Overwrite)

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

async function writeFile() {
  const content = 'Hello, World!';
  await fs.writeFile('output.txt', content, 'utf8');
  console.log('File written successfully');
}

writeFile();

Append (Add to End)

async function appendFile() {
  await fs.appendFile('log.txt', 'New log entry\n', 'utf8');
  console.log('Content appended');
}

Write JSON

async function writeJSON() {
  const data = {
    users: [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ]
  };
  
  const json = JSON.stringify(data, null, 2);
  await fs.writeFile('users.json', json, 'utf8');
}

Read JSON

async function readJSON() {
  const content = await fs.readFile('users.json', 'utf8');
  const data = JSON.parse(content);
  console.log(data.users[0].name);  // "Alice"
}

Checking Files Exist

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

async function checkFile(filepath) {
  try {
    await fs.access(filepath);
    console.log('File exists');
    return true;
  } catch {
    console.log('File does not exist');
    return false;
  }
}

checkFile('data.txt');

File Stats (Metadata)

async function getFileInfo(filepath) {
  const stats = await fs.stat(filepath);
  
  console.log('Size:', stats.size, 'bytes');
  console.log('Created:', stats.birthtime);
  console.log('Modified:', stats.mtime);
  console.log('Is file:', stats.isFile());
  console.log('Is directory:', stats.isDirectory());
}

getFileInfo('data.txt');

Deleting Files

async function deleteFile(filepath) {
  try {
    await fs.unlink(filepath);
    console.log('File deleted');
  } catch (err) {
    console.error('Error deleting file:', err);
  }
}

deleteFile('temp.txt');

Working with Directories

Create Directory

async function createDir() {
  // Create single directory
  await fs.mkdir('uploads');
  
  // Create nested directories
  await fs.mkdir('data/users/profiles', { recursive: true });
  
  console.log('Directories created');
}

Read Directory

async function listFiles(dirpath) {
  const files = await fs.readdir(dirpath);
  
  console.log('Files in directory:');
  files.forEach(file => console.log(file));
}

listFiles('.');

Read Directory with Details

async function listFilesDetailed(dirpath) {
  const entries = await fs.readdir(dirpath, { withFileTypes: true });
  
  for (const entry of entries) {
    const type = entry.isDirectory() ? 'DIR ' : 'FILE';
    console.log(`${type} ${entry.name}`);
  }
}

listFilesDetailed('.');

Delete Directory

async function deleteDir(dirpath) {
  // Delete empty directory
  await fs.rmdir(dirpath);
  
  // Delete directory and contents (be careful!)
  await fs.rm(dirpath, { recursive: true, force: true });
  
  console.log('Directory deleted');
}

Path Module

Work with file paths correctly across operating systems:

const path = require('path');

// Join path segments
const filepath = path.join('data', 'users', 'profile.json');
// Windows: data\users\profile.json
// Mac/Linux: data/users/profile.json

// Get absolute path
const absolute = path.resolve('data', 'users.json');
// /Users/you/project/data/users.json

// Get filename
path.basename('/data/users/alice.json');  // "alice.json"

// Get extension
path.extname('document.pdf');  // ".pdf"

// Get directory
path.dirname('/data/users/alice.json');  // "/data/users"

// Parse path
const parsed = path.parse('/data/users/alice.json');
// {
//   root: '/',
//   dir: '/data/users',
//   base: 'alice.json',
//   ext: '.json',
//   name: 'alice'
// }

// Current file's directory (always use this for relative paths)
const dataPath = path.join(__dirname, 'data', 'users.json');

Real-World Examples

Log File Manager

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

class Logger {
  constructor(logDir = 'logs') {
    this.logDir = logDir;
  }
  
  async init() {
    await fs.mkdir(this.logDir, { recursive: true });
  }
  
  async log(message, level = 'INFO') {
    const timestamp = new Date().toISOString();
    const logEntry = `[${timestamp}] ${level}: ${message}\n`;
    
    const date = new Date().toISOString().split('T')[0];
    const logFile = path.join(this.logDir, `${date}.log`);
    
    await fs.appendFile(logFile, logEntry);
  }
  
  async getRecentLogs(days = 7) {
    const files = await fs.readdir(this.logDir);
    const logFiles = files.filter(f => f.endsWith('.log'));
    
    const logs = [];
    for (const file of logFiles.slice(-days)) {
      const content = await fs.readFile(
        path.join(this.logDir, file),
        'utf8'
      );
      logs.push({ file, content });
    }
    
    return logs;
  }
}

// Usage
const logger = new Logger();
await logger.init();
await logger.log('Application started');
await logger.log('Database connected');

Config File Manager

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

class Config {
  constructor(filename = 'config.json') {
    this.filepath = path.join(__dirname, filename);
    this.data = {};
  }
  
  async load() {
    try {
      const content = await fs.readFile(this.filepath, 'utf8');
      this.data = JSON.parse(content);
    } catch (err) {
      // File doesn't exist, use defaults
      this.data = {};
    }
  }
  
  async save() {
    const json = JSON.stringify(this.data, null, 2);
    await fs.writeFile(this.filepath, json, 'utf8');
  }
  
  get(key, defaultValue = null) {
    return this.data[key] ?? defaultValue;
  }
  
  set(key, value) {
    this.data[key] = value;
  }
}

// Usage
const config = new Config();
await config.load();
config.set('apiUrl', 'https://api.example.com');
config.set('timeout', 5000);
await config.save();

File Processing Pipeline

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

async function processTextFiles(inputDir, outputDir) {
  // Ensure output directory exists
  await fs.mkdir(outputDir, { recursive: true });
  
  // Read all files
  const files = await fs.readdir(inputDir);
  const textFiles = files.filter(f => f.endsWith('.txt'));
  
  // Process each file
  for (const filename of textFiles) {
    const inputPath = path.join(inputDir, filename);
    const outputPath = path.join(outputDir, filename);
    
    // Read content
    const content = await fs.readFile(inputPath, 'utf8');
    
    // Process (example: convert to uppercase)
    const processed = content.toUpperCase();
    
    // Write result
    await fs.writeFile(outputPath, processed, 'utf8');
    
    console.log(`Processed: ${filename}`);
  }
  
  console.log(`Processed ${textFiles.length} files`);
}

processTextFiles('./input', './output');

Stream for Large Files

For very large files, use streams to avoid loading entire file into memory:

const fs = require('fs');

// Create read stream
const readStream = fs.createReadStream('large-file.txt', 'utf8');

readStream.on('data', (chunk) => {
  console.log('Received chunk:', chunk.length, 'bytes');
});

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

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

Best Practices

DO:

  • Use promises over callbacks for async operations
  • Use path.join() for cross-platform paths
  • Use __dirname for relative paths
  • Handle errors with try/catch
  • Use streams for large files
  • Validate file paths before operations

DON’T:

  • Use synchronous methods in server code
  • Hardcode file paths
  • Forget to handle errors
  • Load huge files into memory
  • Trust user-provided file paths (security!)

Summary

Operation Async Sync
Read file fs.readFile() fs.readFileSync()
Write file fs.writeFile() fs.writeFileSync()
Append fs.appendFile() fs.appendFileSync()
Delete file fs.unlink() fs.unlinkSync()
Check exists fs.access() fs.accessSync()
Get stats fs.stat() fs.statSync()
Read directory fs.readdir() fs.readdirSync()
Make directory fs.mkdir() fs.mkdirSync()