Work With Files in Node.js

Node.js·26 min read·Jan 1, 2025

In Node.js, most scripts and applications require access to the files stored on the disk of the machine they run on.

Common use cases include, reading from configuration files, processing and converting data files (e.g., JSON, CSV), generating execution logs, creating automated backups, and more.

The fs module

The fs module (short for "file system") provides several APIs for interacting with the filesystem in a way modeled on standard POSIX functions.

Each API of the fs module has a callback, synchronous, and promise form, where:

  • The callback and synchronous APIs are accessible using the node:fs import.
  • The promise APIs are accessible using the node:fs/promises import.

The callback APIs have the following signature:

fs.api(...params, callback)

The synchronous APIs have the following signature:

fs.apiSync(...params)

The promise APIs have the following signature:

fsPromises.api(...params)

Note: For the sake of simplicity and brevity, we'll only cover the synchronous version of the APIs and their most commonly used options.

Get information about a file

To retrieve information about a file, you can use the fs.statSync() method:

const stats = fs.statSync(path);

Where:

  • stats is a fs.Stats object.
  • path is the path to the target file.

This method returns a fs.Stats object that has built-in methods for validating the file type, such as isFile() to check if the file is a regular file, and properties, such as size to check its size in bytes.

Note: It is similar to the Unix stats command.

Example

Let's consider this script that retrieves information about the script file itself:

const fs = require('node:fs');

try {
  const stats = fs.statSync(__filename);

  console.log('File type: ' + (stats.isFile() ? 'regular file' : 'other'));
  console.log('File owner: ' + stats.uid);
  console.log('File size: ' + stats.size);
  console.log('File creation: ' + stats.birthtime);
} catch(error) {
  console.error(error);
}

💡 Tip: In Node.js, the special __filename variable contains the absolute path to the current module.

When executed, it will:

  1. Declare a stats variable and initialize it with the fs.Stats object returned by the call to the fs.statSync() method.

  2. Output information about the file, derived from the stats object.

Which will produce this output:

File type: regular file
File owner: 501
File size: 278
File creation: Tue Sep 03 2024 13:42:48 GMT+0200 (Central European Summer Time)

Read the content of a file

To read and store the entire content of a file into a variable, you can use the fs.readFileSync() static method:

const content = fs.readFileSync(path, { encoding? });

Where:

  • content is either a string or buffer.
  • path is the path to the target file.
  • encoding is an optional string used to specify the character encoding (e.g., 'utf8').

Note: This method will return a string if the encoding is specified, or a Buffer object otherwise.

Example

Let's consider this CSV file named leads.csv located in the current directory:

first_name,last_name,email_address
John,Doe,johndoe@email.com
Alice,Kepler,a.kepler@email.com
Marvin,Ianopoulos,mrvinpls33@email.com

Let's consider this script that extracts email addresses from a CSV file:

const fs = require('node:fs');

try {
  const leads = fs.readFileSync('leads.csv', { encoding: 'utf8' });

  const emails = leads.split('\n').reduce((emails, row) => {
    const emailRegex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
    const columns = row.split(',');

    if (emailRegex.test(columns[2])) {
      emails.push(columns[2]);
    }
    return emails;
  }, []);

  console.log(emails);
} catch(error) {
  console.error(error);
}

When executed, it will:

  1. Declare a variable named leads and initialize it with the UTF-8-encoded string returned by the fs.readFileSync() method.
  2. Split the string contained in the leads variable into an array of strings using the newline character \n as delimiter.
  3. Split each substring into an array of strings using the comma character , as delimiter.
  4. Check if the 3rd substring is a valid email address and add it to the email array.
  5. Output the list of emails.

Which will produce this output:

[ 'johndoe@email.com', 'a.kepler@email.com', 'mrvinpls33@email.com' ]

Write data into a file

To write data into a file, you can use the writeFileSync() static method:

fs.writeFileSync(path, data, { encoding?, mode? });

Where:

  • path is the path to the target file.
  • data is a string or buffer to write to the file.
  • encoding is an optional string used to specify the character encoding. Defaults to 'utf8'.
  • mode is an optional octal number used to specify the file permissions. Defaults to 0o666.

Alternatively, to append data to a file, you can use the fs.appendFileSync() static method:

fs.appendFileSync(path, data, { encoding?, mode? });

Notes:

  • The fs.writeFileSync() method will completely overwrite the file.
  • Both the fs.writeFileSync() and fs.appendFileSync() methods will automatically create the file if it doesn't exist.

Example

Let's consider this script that generates random hero names:

const fs = require('node:fs');

const adjectives = ['Mighty', 'Shadow', 'Galactic', 'Spectral', 'Crimson'];
const nouns = ['Warrior', 'Guardian', 'Ranger', 'Knight', 'Sentinel'];

function generateHeroName() {
  const randomAdjective = adjectives[Math.floor(Math.random() * adjectives.length)];
  const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];

  return `${randomAdjective} ${randomNoun}`;
}

function generateHeroNames(number = 1) {
  let names = [];
  
  if (number <= 0) {
    return [generateHeroName()];
  }

  while (names.length < 5) {
    let name = generateHeroName();

    if (!names.includes(name)) {
      names.push(name);
    }
  }

  return names;
}

const heroNames = generateHeroNames(5).join('\n') + '\n';

try {
  fs.writeFileSync('heroes.txt', heroNames);
} catch(error) {
  console.error(error);
}

When executed, it will:

  1. Declare a variable named adjectives and initialize it with an array of strings.
  2. Declare a variable named nouns and initialize it with an array of strings.
  3. Define a function named generateHeroName() that randomly selects an entry from the adjectives and nouns arrays, and returns a concatenated string of both values representing a hero name.
  4. Define a function named generateHeroNames() that invokes the generateHeroName() function, adds the hero name to the names array if it doesn't exist, and returns the array.
  5. Declare a variable named heroNames and initialize it with the concatenated list of hero names generated by the generateHeroNames() function.
  6. Attempt to write the content of the heroNames variable into the file named heroes.txt.

Which will produce this output:

$ cat heroes.txt
Shadow Knight
Crimson Guardian
Mighty Ranger
Spectral Knight
Shadow Sentinel

Copy a file

To create a copy of a regular file, you can use the fs.copyFileSync() static method:

fs.copyFileSync(source, destination);

Where:

  • source is the path to the original file to copy.
  • destination is the path to the destination file to copy it as.

Note: This function is similar to the Unix cp command, which means that if the file already exists, it will be automatically overwritten.

Example

Let's consider this script that creates a backup of data files:

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

function getCurrentDate() {
  const date = new Date();
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, '0');
  const day = String(date.getDate()).padStart(2, '0');

  return `${year}${month}${day}`;
}

const backupDirectory = './backups';
const dataDirectory = './data';
const files = ['customers.csv', 'orders.csv'];

try {
  for (const file of files) {
    const source = path.join(dataDirectory, file);
    const destination = path.join(backupDirectory, `${getCurrentDate()}_${file}`);

    console.log(`Copying '${source}' -> '${destination}'`);
    
    fs.copyFileSync(source, destination);
    
    console.log('Done.');
  }
} catch(error) {
  console.error(error);
}

When executed, it will:

  1. Define a function named getCurrentDate() that returns the current date in the YYYYMMDD format using the Date object.
  2. Declare a variable named backupDirectory and initialize it with the path to the directory the files will be copied to.
  3. Declare a variable named dataDirectory and initialize it with the path to the directory the files will be copied from.
  4. Declare a variable named files and initialize it with the list of filenames to copy.
  5. Iterate on each file using a for...of loop.
  6. Declare a variable named source and initialize it with the full path to the original file.
  7. Declare a variable named destination and initialize it with the full path to the copy file, prefixed with the current date.
  8. Copy the source into the destination using the fs.copyFileSync() method.

Which will produce this output:

Copying 'data/customers.csv' -> 'backups/20241114_customers.csv'
Done.
Copying 'data/orders.csv' -> 'backups/20241114_orders.csv'
Done.

Remove a file

To remove a file, you can use the fs.rmSync() method:

fs.rmSync(path, { force? });

Where:

  • path is the path to the file you want to remove.
  • force is an optional boolean used to ignore exceptions when path doesn't exist.

Note: This function is similar to the Unix rm command.

Example

Let's consider this script that removes files older than a certain amount of days:

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

function isOlderThan(creationMs, days) {
  const currentMs = Date.now();
  const daysMs = days * 24 * 60 * 60 * 1000;

  return currentMs - creationMs > daysMs;
}

const directory = './logs';
const files = ['usage.log', 'error.log'];

try {
  for (let file of files) {
    const filepath = path.join(directory, file);
    const stats = fs.statSync(filepath);

    if (stats.isFile() && isOlderThan(stats.birthtimeMs, 30)) {
      console.log(`Deleting file: '${filepath}'...`);
      fs.rmSync(path);
      console.log('Done');
    } else {
      console.log(`Skipping file: '${filepath}'`);
    }
  }
} catch(error) {
  console.error(error);
}

When executed, it will:

  1. Define a function named isOlderThan() that takes as arguments a file creation time in milliseconds and a number of days, and returns whether the file is older than the number of days.
  2. Declare a variable named directory and initialize it with the path string of the base directory to remove files from.
  3. Declare a variable named files and initialize it with an array of filenames to check and remove.
  4. Iterate on each file using a for...of loop.
  5. Declare a variable named filepath and initialize it with the full path to the current file.
  6. Declare a variable named stats and initialize it with the fs.Stats object of this file.
  7. Attempt to remove the file if it is a regular file and it is older than 30 days.

Which will produce this output:

Skipping file: 'data/usage.log'
Deleting file: 'logs/error'...
Done

🗒️ Summary

Here's a summary of what you've learned in this lesson:

  • The node:fs core module provides APIs for interacting with the filesystem.
  • The fs.statSync() method retrieves information about a file.
  • The fs.readFileSync() method returns the content of a file as a string or Buffer.
  • The fs.writeFileSync() method writes a string or Buffer into a file.
  • The fs.appendFileSync() method writes a string or Buffer at the end of a file.
  • The fs.copyFileSync() method copies a file into another file.
  • The fs.rmSync() method removes a file.