JavaScript has long been one of the most popular scripting languages, but for an extended period of time, it was not a great choice for server-side backend application development. Then came Node.js, which is used to create server-side, event-driven, lightweight applications built using the JavaScript programming language.

Node.js is an open-source JavaScript runtime that is available to download and install for free on any of the top operating systems (Windows, Mac, Linux). It’s grown increasingly popular with app creators in recent years, and it’s supplied many new employment opportunities for JavaScript developers seeking a specialty.

In this article, we will learn about managing the file system using Node.js. It’s effortless to use Node.js APIs to interact with the file system and carry out many complex operations, and knowing how to maneuver through them will boost your productivity.

Let’s dive in!

Prerequisites to Understanding the Node.js File System

The primary prerequisite is Node.js installation on your operating system. Node.js doesn’t require any complex hardware to run, making it easy to download and install Node.js on your computer.

It would help if you also have a basic knowledge of JavaScript to work on Node.js modules like file systems (also known as “FS” or “fs”). A high-level understanding of JavaScript functions, callback functions, and promises will help you get a grip on this subject even faster.

Node.js File System Module

Working with files and directories is one of the basic needs of a full-stack application. Your users may want to upload images, resumes, or other files to a server. At the same time, your application may need to read configuration files, move files, or even change their permissions programmatically.

The Node.js file system module has got all these covered. It provides several APIs to interact with the file systems seamlessly. Most of the APIs are customizable with options and flags. You can also use them to perform both synchronous and asynchronous file operations.

Before we take a deep dive into the file system module, let’s have a sneak peek at what the Node.js module is all about.

Node.js Modules

Node.js modules are a set of functionalities available as APIs for a consumer program to use. For example, you’ve got the fs module to interact with the file system. Likewise, an http module utilizes its functions to create a server and many more operations. Node.js offers plenty of modules to abstract many low-level functionalities for you.

You can make your own modules too. With Node.js version 14 and onwards, you can create and use Node.js modules in two ways: CommonJS (CJS) and ESM (MJS) modules. All the examples we will see in this article are in the CJS style.

Working With Files in Node.js

Working with files involves various operations with files and directories (folders). Now we will learn about each of these operations with example source code. Please open your favorite source code editor and try them out as you read along.

First, import the fs module to your source file to start working with the file system methods. In the CJS style, we use the require method to import a method from a module. So, to import and use the fs module methods, you would do this:

const { writeFile } = require('fs/promises');

Also, note we are importing the method writeFile from the fs/promises package. We want to use the promisified methods as they are the latest, and they’re easy to use with async/await keywords and lesser code. Other alternatives are the synchronous methods and the callback functions that we will see later.

How To Create and Write to a File

You can create and write to a file in three ways:

  1. Using the writeFile method
  2. Using the appendFile method
  3. Using the openFile method

These methods accept a file path and the data as content to write in the file. If the file exists, they replace the content in the file. Otherwise, they create a new file with the content.

1. Using the writeFile Method

The code snippet below shows the usage of the writeFile method. Start by creating a file called createFile.js using the snippet below:

const { writeFile } = require('fs/promises');
async function writeToFile(fileName, data) {
  try {
    await writeFile(fileName, data);
    console.log(`Wrote data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to write the file: ${error.message}`);
  }
}

Note we are using the await keyword to invoke the method as it returns a JavaScript promise. A successful promise will create/write to the file. We have a try-catch block to handle errors in case the promise is rejected.

Now we can invoke the writeToFile function:

writeToFile('friends.txt', 'Bob');

Next, open a command prompt or terminal and run the above program using the following command:

node createFile.js

It will create a new file called friends.txt with a line that simply says:

Bob

2. Using the appendFile Method

As the name indicates, the primary usage of this method is to append and edit a file. However, you can also use the same method to create a file.

Take a look at the function below. We use the appendFile method with the w flag to write a file. The default flag for appending to a file is a:

const { appendFile} = require('fs/promises');

async function appendToFile(fileName, data) {
  try {
    await appendFile(fileName, data, { flag: 'w' });
    console.log(`Appended data to ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to append the file: {error.message}`);
  }
}

Now, you can invoke the above function like this:

appendToFile('activities.txt', 'Skiing');

Next, you can execute the code in the Node.js environment using the node command, as we saw earlier. This will create a file called activities.txt with the content Skiing in it.

3. Using the open Method

The last method we will learn for creating and writing to a file is the open method. You can open a file using the w (for “write”) flag:

const { open} = require('fs/promises');

async function openFile(fileName, data) {
  try {
    const file = await open(fileName, 'w');
    await file.write(data);
    console.log(`Opened file ${fileName}`);
  } catch (error) {
    console.error(`Got an error trying to open the file: {error.message}`);
  }
}

Now invoke the openFile function with:

openFile('tasks.txt', 'Do homework');

When you run the script using the node command, you will have a file named tasks.txt created with the initial content:

Do homework

How To Read a File

Now that we know how to create and write to a file, let’s learn to read file content. We’ll use the readFile method from the file system module to do so.

Create a file called readThisFile.js with the following code:

// readThisFile.js
const { readFile } = require('fs/promises');
async function readThisFile(filePath) {
  try {
    const data = await readFile(filePath);
    console.log(data.toString());
  } catch (error) {
    console.error(`Got an error trying to read the file: {error.message}`);
 }
}

Now let’s read all the three files we’ve created by invoking the readThisFile function:

readThisFile('activities.txt');
readThisFile('friends.txt');
readThisFile('tasks.txt');

Finally, execute the script using the following node command:

node readThisFile.js

You should see the following output in the console:

Skiing
Do homework
Bob

A point to note here: The readFile method reads the file asynchronously. This means the order you read the file and the order you get a response to print in the console may not be the same. You have to use the synchronous version of the readFile method to get it in order. We’ll see that here in a little while.

How To Rename a File

To rename a file, use the rename method from the fs module. Let’s create a file called rename-me.txt. We’ll rename this file programmatically.

Create a file called renameFile.js with the following code:

const { rename } = require('fs/promises');

async function renameFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Renamed ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to rename the file: ${error.message}`);
  }
}

As you may have noticed, the rename method takes two arguments. One is the file with the source name, and the other is the target name.

Now let’s invoke the above function to rename the file:

const oldName = "rename-me.txt";
const newName = "renamed.txt";
renameFile(oldName, newName);

Like before, execute the script file using the node command to rename the file:

node renameFile.js

How To Move a File

Moving a file from one directory to another is similar to renaming its path. So, we can use the rename method itself to move files.

Let’s create two folders, from and to. Then we’ll create a file called move-me.txt inside the from folder.

Next, we’ll write the code to move the move-me.txt file. Create a file called moveFile.js with the following snippet:

const { rename } = require('fs/promises');
const { join } = require('path');
async function moveFile(from, to) {
  try {
    await rename(from, to);
    console.log(`Moved ${from} to ${to}`);
  } catch (error) {
    console.error(`Got an error trying to move the file: ${error.message}`);
  }
}

As you can see, we’re using the rename method just like before. But why do we need to import the join method from the path module (yes, the path is another crucial module of Node.js)?

The join method is used to join several specified path segments to form one path. We’ll use it to form the path of source and destination file names:

const fromPath = join(__dirname, "from", "move-me.txt");
const destPath = join(__dirname, "to", "move-me.txt");
moveFile(fromPath, destPath);

And that’s it! If you execute the moveFile.js script, you will see the move-me.txt file moved to the to folder.

How To Copy a File

We use the copyFile method from the fs module to copy a file from the source to the destination.

Take a look at the code snippet below:

const { copyFile } = require('fs/promises');
const { join } = require('path');
async function copyAFile(from, to) {
  try {
    await copyFile(from, to);
    console.log(`Copied ${from} to ${to}`);
  } catch (err) {
    console.error(`Got an error trying to copy the file: ${err.message}`);
  }
}

Now you can invoke the above function with:

copyAFile('friends.txt', 'friends-copy.txt');

It will copy the content of the friends.txt to the friends-copy.txt file.

Now, that’s great, but how do you copy multiple files?

You can use the Promise.all API to execute multiple promises in parallel:

async function copyAll(fromDir, toDir, filePaths) {
  return Promise.all(filePaths.map(filePath => {
   return copyAFile(join(fromDir, filePath), join(toDir, filePath));
  }));
}

Now you can supply all the file paths to copy from one directory to another:

copyFiles('from', 'to', ['copyA.txt', 'copyB.txt']);

You can also use this approach to perform other operations like moving, writing, and reading files in parallel.

How To Delete a File

We use the unlink method to delete a file:

const { unlink } = require('fs/promises');
async function deleteFile(filePath) {
  try {
    await unlink(filePath);
    console.log(`Deleted ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to delete the file: ${error.message}`);
  }
}

Remember, you’ll need to provide the path to the file to delete it:

deleteFile('delete-me.txt');

Note that if the path is a symlink to another file, the unlink method will cancel the symlink, but the original file will be untouched. We’ll talk more about symlinks later on.

How To Change File Permissions and Ownership

You may at some point want to change file permissions programmatically. This can come in very helpful for making a file read-only or fully accessible.

We’ll use the chmod method to change the permission of a file:

const { chmod } = require('fs/promises');
async function changePermission(filePath, permission) {
  try {
    await chmod(filePath, permission);
    console.log(`Changed permission to ${permission} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change permission: ${error.message}`);
  }
}

We can pass the file path and the permission bitmask to change the permission.

Here is the function call to change the permission of a file to read-only:

changePermission('permission.txt', 0o400);

Similar to permission, you can also change the ownership of a file programmatically. We use the method chown to do that:

const { chown } = require('fs/promises');

async function changeOwnership(filePath, userId, groupId) {
  try {
    await chown(filePath, userId, groupId);
    console.log(`Changed ownership to ${userId}:${groupId} for ${filePath}`);
  } catch (error) {
    console.error(`Got an error trying to change ownership: ${error.message}`);
  }
}

Then we call the function with the file path, user ID, and group ID:

changeOwnership('ownership.txt', 1000, 1010);

How To Create a Symlink

The symbolic link (also known as symlink) is a filesystem concept to create a link to a file or folder. We create symlinks to create shortcuts to a target file/folder in the file system. The Node.js filesystem module provides the symlink method to create a symbolic link.

To create the symlink, we need to pass the target file path, actual file path, and type:

const { symlink } = require('fs/promises');
const { join } = require('path');
async function createSymlink(target, path, type) {
  try {
    await symlink(target, path, type);
    console.log(`Created symlink to ${target} at ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the symlink: ${error.message}`);
  }
}

We can invoke the function with:

createSymlink('join(__dirname, from, symMe.txt)', 'symToFile', 'file');

Here we’ve created a symlink called symToFile.

How To Watch Changes to a File

Did you know that you can watch the changes happening to a file? It’s a great way to monitor alterations and events, especially when you’re not expecting them. You can capture and audit these for later review.

The watch method is the best way to watch file changes. There is an alternate method called watchFile, but it’s not as performant as the watch method.

So far, we’ve used the file system module method with async/await keywords. Let’s see the usages of the callback function with this example.

The watch method accepts the file path and a callback function as arguments. Whenever an activity takes place on the file, the callback function gets called.

We can take advantage of the event parameter to get more information about the activities:

const fs = require('fs');
function watchAFile(file) {
  fs.watch(file, (event, filename) => {
    console.log(`${filename} file Changed`);
  });
}

Invoke the function by passing a file name to watch:

watchAFile('friends.txt');

Now we’re automatically capturing any activities on the friends.txt file.

Working With Directories (Folders) in Node.js

Let’s now move on to learning how to perform operations on directories or folders. Many of the operations like rename, move, and copy are similar to what we’ve seen for files. However, specific methods and operations are usable only on directories.

How To Create a Directory

We use the mkdir method to create a directory. You need to pass the directory name as an argument:

const { mkdir } = require('fs/promises');
async function createDirectory(path) {
  try {
    await mkdir(path);
    console.log(`Created directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to create the directory: ${error.message}`);
  }
}

Now we can invoke the createDirectory function with a directory path:

createDirectory('new-directory');

This will create a directory named new-directory.

How To Create a Temporary Directory

Temporary directories are not regular directories. They have a special meaning to the operating system. You can create a temporary directory using the mkdtemp() method.

Let’s create a temporary folder within the temporary directory of your operating system. We get the information for the temporary directory location from the tmpdir() method of the os module:

const { mkdtemp } = require('fs/promises');
const { join } = require('path');
const { tmpdir } = require('os');
async function createTemporaryDirectory(fileName) {
  try {
    const tempDirectory = await mkdtemp(join(tmpdir(), fileName));
    console.log(`Created temporary directory ${tempDirectory}`);
  } catch (error) {
    console.error(`Got an error trying to create the temporary directory: ${error.message}`);
  }
}

Now, let’s call the function with the directory name to create it:

createTemporaryDirectory('node-temp-file-');

Note that Node.js will add six random characters at the end of the created temporary folder name to keep it unique.

How To Delete a Directory

You need to use the rmdir() method to remove/delete a directory:

const { rmdir } = require('fs/promises');
async function deleteDirectory(path) {
  try {
    await rmdir(path);
    console.log(`Deleted directory ${path}`);
  } catch (error) {
    console.error(`Got an error trying to delete the directory: ${error.message}`);
  }
}

Next, call the above function by passing the path of the folder you want to remove:

deleteDirectory('new-directory-renamed');

Synchronous vs Asynchronous APIs

So far, we’ve seen plenty of examples of file system methods, and all of them are with asynchronous usages. However, you may need to handle some operations synchronously.

One example of synchronous operation is reading multiple files one after another. The fs module has a method called readFileSync() to get it done:

const { readFileSync } = require('fs');
function readFileSynchronously(path) {
  try {
    const data = readFileSync(path);
    console.log(data.toString());
  } catch (error) {
    console.error(error);
  }
}

Note the readFileSync() method is not required from the “fs/promises” package. This is because the method is not asynchronous. So, you can call the above function with:

readFileSynchronously('activities.txt');
readFileSynchronously('friends.txt');
readFileSynchronously('tasks.txt');

In this case, all the above files will be read in the order the functions were called.

The Node.js file system module offers a synchronous method for other operations like the read operation. Use them wisely and only on an as-needed basis. The asynchronous methods are far more helpful for parallel execution.

Handling Errors

As any coder knows, you have to expect errors and be ready to handle them when performing a file or directory operation. What if the file is not found, or you have no permission to write to a file? There can (and probably will) be many cases where you could encounter an error.

You should always surround your method calls with a try-catch block. This way, if an error occurs, the control will go over to the catch block, where you can review and handle the error. As you may have noticed in all the examples above, we’ve been using the try-catch block in handling errors we came across.

Summary

Let’s review the key points we’ve covered in this tutorial:

  • The Node.js file system (fs) module has many methods to help with many low-level tasks.
  • You can perform various file operations like create, write, rename, copy, move, delete, and many more.
  • You can do several directory operations like create, temporary directory, move, and many more.
  • All the methods can be invoked asynchronous way using JavaScript promises or callback functions.
  • You can also synchronously invoke the methods if needed.
  • Always prefer the asynchronous methods over synchronous.
  • Handle errors with a try-catch block every time you interact with the methods.

Now that we’ve worked with the Node.js file system a bit, you should have a good handle on its ins and outs. If you want to further beef up your know-how, you may want to look into the Node.js streams as a natural progression of learning Node.js modules. Streams are efficient ways to handle information exchange, including network calls, file read/write, and much more.

You can find all the source code used in this article in this GitHub Repository.

Are you planning on using Node.js for your next project? Let us know why you picked it in the comments section below.