DEV Community

Ansh Sheladiya
Ansh Sheladiya

Posted on

Node.js File System Operations: Read, Write, Stream, and Manage Files

File system operations are a fundamental part of building reliable Node.js applications. Whether you are processing uploaded documents, generating reports, managing configuration files, or implementing background jobs, understanding how Node.js interacts with files and directories helps you build more efficient backend systems.

Node.js provides the built-in fs module for working with the file system without installing additional dependencies. It supports synchronous operations, callback-based APIs, Promise-based methods, file streams, directory management, and file metadata operations.

In this article, we will explore practical file system operations using node:fs/promises, handle errors correctly, work with directories, and process larger files efficiently. We will also build a runnable example that demonstrates how these techniques fit together in a real-world workflow.

Working with the Node.js File System API

The Node.js file system module provides several ways to interact with files. The traditional node:fs module supports callback-based and synchronous operations, while node:fs/promises offers Promise-based methods that work naturally with async and await. For most application code, the Promise API makes asynchronous workflows easier to read and maintain without blocking the event loop while waiting for file operations to complete.

Common operations include readFile() for reading file contents, writeFile() for creating or replacing files, appendFile() for adding content, mkdir() for creating directories, readdir() for listing directory entries, stat() for inspecting file metadata, and rename() for moving files. The unlink() method removes files, while rm() can remove files or directories with appropriate options. Always handle expected errors, such as ENOENT for missing paths and EACCES for permission problems, instead of assuming every operation succeeds.

File paths deserve special attention in production applications. Use node:path methods such as join() and resolve() rather than manually concatenating path strings, and remember that relative paths are resolved against the process's current working directory. When handling user-provided paths, validate them against an approved base directory to prevent path traversal. Avoid blindly trusting filenames, and use restrictive permissions when working with sensitive files.

For large files, prefer streams over loading the entire file into memory with readFile(). Streams process data incrementally, helping applications handle large logs, exports, and uploads with a smaller memory footprint. The following example demonstrates directory creation, file writing, reading, appending, metadata inspection, directory listing, and cleanup using the Promise-based API.

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

// Keep all example files inside a dedicated working directory.
const dataDirectory = path.resolve(process.cwd(), 'fs-demo-data');
const reportPath = path.join(dataDirectory, 'report.txt');
const jsonPath = path.join(dataDirectory, 'report.json');

async function runFileSystemDemo() {
  console.log('\n[1] Starting the file system demonstration');
  console.log('Working directory:', dataDirectory);

  try {
    // Create the directory if it does not already exist.
    await fs.mkdir(dataDirectory, { recursive: true });
    console.log('[2] Directory created or already exists');

    // Write initial content to a text file.
    const initialContent = [
      'Node.js File System Report',
      'Status: Initialized',
      'Environment: Development'
    ].join('\n');
    await fs.writeFile(reportPath, initialContent, 'utf8');
    console.log('[3] Initial report written successfully');

    // Read the complete text file asynchronously.
    const content = await fs.readFile(reportPath, 'utf8');
    console.log('[4] Current report contents:\n' + content);

    // Append a new line without replacing existing content.
    await fs.appendFile(reportPath, '\nStatus: Processing complete', 'utf8');
    console.log('[5] Additional status appended');

    // Store structured data as formatted JSON.
    const reportData = {
      title: 'File System Demonstration',
      generatedAt: new Date().toISOString(),
      operations: ['mkdir', 'writeFile', 'readFile', 'appendFile'],
      completed: true
    };
    await fs.writeFile(jsonPath, JSON.stringify(reportData, null, 2), 'utf8');
    console.log('[6] JSON report saved successfully');

    // Read and parse JSON when structured data is needed.
    const jsonContent = await fs.readFile(jsonPath, 'utf8');
    const parsedReport = JSON.parse(jsonContent);
    console.log('[7] Parsed report title:', parsedReport.title);
    console.log('[8] Completed operations:', parsedReport.operations.join(', '));

    // Inspect file metadata, including size and modification time.
    const fileStats = await fs.stat(reportPath);
    console.log('[9] Report size:', fileStats.size, 'bytes');
    console.log('[10] Last modified:', fileStats.mtime.toISOString());
    console.log('[11] Is a regular file?', fileStats.isFile());

    // List entries in the working directory.
    const entries = await fs.readdir(dataDirectory, { withFileTypes: true });
    console.log('[12] Directory entries:');
    for (const entry of entries) {
      const entryType = entry.isDirectory() ? 'directory' : 'file';
      console.log(`  - ${entry.name} (${entryType})`);
    }

    // Demonstrate how to check whether a file exists by reading it.
    try {
      await fs.access(reportPath);
      console.log('[13] Report file is accessible');
    } catch (error) {
      if (error.code === 'ENOENT') {
        console.log('[13] Report file does not exist');
      } else {
        throw error;
      }
    }

    // Read the final content after all updates.
    const finalContent = await fs.readFile(reportPath, 'utf8');
    console.log('[14] Final report:\n' + finalContent);

    // Remove generated files before removing their parent directory.
    await fs.unlink(reportPath);
    console.log('[15] Text report removed');
    await fs.unlink(jsonPath);
    console.log('[16] JSON report removed');
    await fs.rmdir(dataDirectory);
    console.log('[17] Demo directory removed');
  } catch (error) {
    // Log useful error details without hiding the original failure.
    console.error('[ERROR] File system operation failed');
    console.error('Error code:', error.code || 'UNKNOWN');
    console.error('Message:', error.message);
    process.exitCode = 1;
  }
}

// Run the asynchronous workflow and report unexpected failures.
runFileSystemDemo().catch((error) => {
  console.error('Unexpected error:', error);
  process.exitCode = 1;
});
Enter fullscreen mode Exit fullscreen mode

Conclusion

Node.js file system operations are essential for building backend services that interact with persistent data. By using node:fs/promises, you can write readable asynchronous workflows while keeping the event loop responsive during file I/O. Combining these APIs with node:path also makes path handling more consistent across operating systems.

For production applications, choose the right API for the workload: use readFile() for reasonably sized files, streams for large data, and explicit error handling for operations that may fail. Validate user-controlled paths, avoid exposing sensitive filesystem locations in error responses, and consider atomic write strategies when file integrity matters.

Start by mastering directory creation, file reading, writing, appending, metadata inspection, and cleanup. Once these fundamentals are comfortable, explore file streams, watchers, temporary files, and safe file-upload handling to build more robust Node.js applications.

Top comments (0)