File System in Node.js Part 1: Working with Folders

March 12, 2021

One of the most common and practical operations that you could perform while working with JavaScript in Node.js is interacting with files and folders on your file system. Think of actions such as creating a folder (directory), creating a file, and reading a file.

There are two ways of interacting with the operating system when using Node.js.

  • One of them is synchronously.
  • And the other one is asynchronously.

We will start by looking at how things work synchronously and then see the difference between these two modes down the line.

Performing Folder Operations

Create a Folder if it Does Not Exist

Let's start by looking at how to perform certain folder operations in Node.js.

We will begin by manually creating a new folder for our project and, inside that folder, create a file called main.js.

mkdir filesystem
cd filesystem
touch main.js

You could perform these operations using the file explorer of your system, but here they are being done through the terminal. By the end of this introduction, you will be able to manipulate the file system programmatically using JavaScript!

Let's dive into the main.js file that we have created. We will first learn how to create a folder programmatically. We will import the fs module, which contains a bunch of functions for file system operations. We will call the mkdirSync function from the fs module to create a folder at the given path. As the Sync suffix suggests, this function makes a folder synchronously.

const fs = require("fs");

const folderPath = "myFolder";

fs.mkdirSync(folderPath);

We can run the code in this file by typing node main.js in the terminal. If you didn't get any messages in the terminal, it means that the code has been successfully executed. You should be able to see the newly created folder.

We will notice that if we tried to rerun this code, it would fail with an error because the folder that we are trying to create already exists on the same path.

Let's add some code to check to see if the folder exists first. We will be using the existsSync function to do that. existsSync function checks to see if a file or a folder exists on the given path.

const fs = require("fs");

const folderPath = "myFolder";

const folderExists = fs.existsSync(folderPath);
console.log(folderExists);

Let's run this program now. It logs the value true if the folder exists and false if not.

Using this knowledge, we can update our code to create the folder only if it exists using an if-else statement.

const fs = require("fs");

const folderPath = "myFolder";

const folderExists = fs.existsSync(folderPath);
if (!folderExists) {
  fs.mkdirSync(folderPath);
} else {
  console.log("Folder exists");
}

Notice how we are making use of variable names that are very easy to understand. Also, notice how we use a positive variable name instead of a negative one as in folderExists and not folderDoesNotExist. When naming variables, we generally want to avoid using negative names, as negating a negative name can make it hard to understand. Check this example here:

if (!folderExists)

It is easier to understand that we are negating the folderExists variable to imply that the folder does not exist. But if the variable name was like this:

if (!folderDoesNotExist)

It becomes harder to think of this variable name when it is negated due to the double negative.

Create a Folder Relative to the Destination File Path

Let's wrap the current functionality we have inside a createFolder function.

const fs = require("fs");

const targetPath = "myFolder";

const createFolder = (folderPath) => {
  const folderExists = fs.existsSync(folderPath);
  if (!folderExists) {
    fs.mkdirSync(folderPath);
    return true;
  } else {
    console.log("Folder exists");
  }

  return false;
};

createFolder(targetPath);

Whenever we call the createFolder function, we would either create a folder or not, depending on if a folder with the given name already exists at the destination that we are trying to create the folder.

The reason to return true or false from this function is to know if the function is completed successfully or not. We can safely assume that the function ended up creating a folder if it returns a true value. Returning a boolean value like this is not something essential to do but can be useful at times.

If we are to go into the myFolder directory and try to run the script from here using a relative path, we will notice that the script creates a folder in the folder that we ran the script from.

cd myFolder

node ../main.js

This behavior might not be desirable. We might want the script to create a folder at the same spot every time that it runs. To ensure that, we can use the __dirname global variable available to every Node.js program.

Let's first see the value of __dirname by logging it in the console.

console.log(__dirname);

As we can see by executing this line of code in our file, the __dirname variable is logging the folder that the script is in regardless of where we run the code from.

Let's make use of this variable in our code.

const fs = require("fs");

const folderName = "myFolder";
const targetPath = `${__dirname}/${folderName}`;

const createFolder = (folderPath) => {
  console.log(`Creating a folder at: ${folderPath}`);

  const folderExists = fs.existsSync(folderPath);
  if (!folderExists) {
    fs.mkdirSync(folderPath);
    console.log("Success!");
    return true;
  } else {
    console.log("Folder already exists");
  }

  return false;
};

createFolder(targetPath);

Using the __dirname variable, our code is now creating a folder in the same directory as the source script every time it runs.

Notice how we are constructing the folder in the previous example by using template literals.

const targetPath = `${__dirname}/${folderName}`;

We could have used the addition operator to construct a string, but template literals are more readable as they don't break the text flow. The same result using the addition operator would have looked like this:

const TARGET_PATH = __dirname + "/" + FOLDER_NAME;

This is not too hard to read in this example, but it can quickly get unwieldy for longer strings.

There is a slight problem with constructing folders in this manner. Different operating systems (like Windows or Mac) might have that slash symbol pointing to backward or forwards, so our code when using a forward slash might fail in other operating systems.

A function called join that lives under the path module can help with normalizing the constructed path for us. Normalizing in this context means it would construct a path that would work regardless of the operating system. Using the join function, we could construct the same path by doing this:

const path = require("path");
const TARGET_PATH = path.join(__dirname, FOLDER_NAME);

This is the final code that makes use of the path.join function.

const fs = require("fs");
const path = require("path");

const folderName = "myFolder";
const targetPath = path.join(__dirname, folderName);

const createFolder = (folderPath) => {
  console.log(`Creating a folder at: ${folderPath}`);

  const folderExists = fs.existsSync(folderPath);
  if (!folderExists) {
    fs.mkdirSync(folderPath);
    console.log("Success!");
    return true;
  } else {
    console.log("Folder already exists");
  }

  return false;
};

createFolder(targetPath);

This function is at a pretty good spot right now. Programmatically creating a single folder might not look like a big deal, given we had to write a bunch of lines of code to achieve this. You might be thinking that doing all this manually could have been much easier. The great thing about doing things programmatically is that once you have created a function like this, there is not much difference in executing it once to create a single folder versus executing it to create thousands of folders. That's the beauty of programming. It gives you a ton of leverage.

Let's adjust our code a little bit to create as many folders as we would like.

Create Multiple Folders

const fs = require("fs");
const path = require("path");

const createFolder = (folderPath) => {
  console.log(`Creating a folder at: ${folderPath}`);

  const folderExists = fs.existsSync(folderPath);
  if (!folderExists) {
    fs.mkdirSync(folderPath);
    console.log("Success!");
    return true;
  } else {
    console.log("Folder already exists");
  }

  return false;
};

const main = () => {
  const parentFolderName = "myFolder";
  const parentFolderPath = path.join(__dirname, parentFolderName);

  const childFolderPrefix = "subFolder_";
  const childFolderAmount = 10;

  createFolder(parentFolderPath);
  for (let i = 0; i < childFolderAmount; i++) {
    const childFolderPath = path.join(
      __dirname,
      parentFolderName,
      `${childFolderPrefix}${i}`
    );
    createFolder(childFolderPath);
  }
};

main();

We first call the createFolder function to create a parent folder containing all the other child folders. And then, we call the createFolder function again inside a for loop to create the child folders (still using the parentFolderName while constructing the path) where the amount of child folders is determined by the childFolderAmount variable.

const childFolderPrefix = "subFolder_";
const childFolderAmount = 10;

createFolder(parentFolderPath);
for (let i = 0; i < childFolderAmount; i++) {
  const childFolderPath = path.join(
    __dirname,
    parentFolderName,
    `${childFolderPrefix}${i}`
  );
  createFolder(childFolderPath);
}

We can make small improvements to this code. One thing that comes to mind is that extracting the createFolder function from this file into a module. Given how useful the createFolder function is, it might make sense to extract it into a separate file to access it from other files if we ever wanted to. This is entirely redundant for our current purposes, but it is a good practice for our learning.

The benefit of extracting the createFolder function to its file is that we can completely ignore the implementation details of this function going forward. We can assume it works as intended and not worry about the inner workings of it. It will help declutter our code and make it easier to read and understand.

Let's create a file called utilities.js and place the createFolder function in there.

const fs = require("fs");

const createFolder = (folderPath) => {
  console.log(`Creating a folder at: ${folderPath}`);

  const folderExists = fs.existsSync(folderPath);
  if (!folderExists) {
    fs.mkdirSync(folderPath);
    console.log("Success!");
    return true;
  } else {
    console.log("Folder already exists");
  }

  return false;
};

module.exports = {
  createFolder,
};

Notice how we are using the module.exports syntax to export the createFolder function. This is the pattern to use whenever we are exporting content from a file.

We will import this function in our main.js file.

const path = require("path");
const { createFolder } = require("./utilities");

const main = () => {
  const parentFolderName = "myFolder";
  const parentFolderPath = path.join(__dirname, parentFolderName);

  const childFolderPrefix = "subFolder_";
  const childFolderAmount = 10;

  createFolder(parentFolderPath);
  for (let i = 0; i < childFolderAmount; i++) {
    const childFolderPath = path.join(
      __dirname,
      parentFolderName,
      `${childFolderPrefix}${i}`
    );
    createFolder(childFolderPath);
  }
};

main();

Notice the main.js looks much smaller now that we are using a module to contain a piece of functionality that can exist separately in a different file. Using modules will make our code easier to read and maintain.

Let's write another useful script before wrapping up this section. What if we wanted to create folders based on some predefined names? Say we wanted to create a folder called project and inside that folder have some subfolders called assets, output, and plugins. Our current script only creates folders with the given prefix. Here is what our script could look like if we wanted to accommodate custom file names:

const path = require("path");
const { createFolder } = require("./utilities");

const main = () => {
  const parentFolderName = "project";
  const subFolderNames = ["assets", "output", "plugins"];

  const parentFolderPath = path.join(__dirname, parentFolderName);
  createFolder(parentFolderPath);

  subFolderNames.forEach((folderName) => {
    const folderPath = path.join(__dirname, parentFolderName, folderName);
    createFolder(folderPath);
  });
};

main();

Of course, we could also create another function that performs this functionality of creating a folder and subfolders for us if we wanted to take this a step further.

Summary

We learned about how to work with folders synchronously using Node.js. We have used the fs.existsSync and fs.mkdirSync functions to check if a folder exists and create the folder if it doesn't. We have also learned about some useful things to keep in mind when programming, such as:

  • Refraining from using negative variable names
  • Using modules to encapsulate functionality.

Avoiding negative variable names would help us write programs that are easier to read and maintain. Using modules to abstract away and encapsulate functionality is one of the most important principles that we can follow when building software.

We have also used loops to create as many folders as we would like and create folders from a given array of names. Once we have a loop that creates a single folder, we can scale that loop to create thousands of folders. This is a great example to the scalability of computer programs.

Find me on Social Media