Node.js fs and path: File System and Path Basics

Node.js includes two core modules that every backend developer uses sooner or later: fs for working with files and directories, and path for building safe, correct file paths. This article explains how they work together, when to use each one, and how to avoid common file handling mistakes.

Quick answer: Use fs to read, write, create, delete, and inspect files and folders. Use path to build file paths in a platform-safe way instead of hardcoding separators like / or \.

Difficulty: Beginner

You'll understand this better if you know: basic JavaScript variables, functions, and how Node.js runs code outside the browser.

1. What Is Node.js fs and path?

The fs module stands for file system. It lets Node.js programs interact with the files and folders on disk. The path module helps you construct, inspect, and normalize file paths without worrying about operating-system differences.

Think of path as the map and fs as the tool that opens the door.

2. Why Node.js fs and path Matter

Most Node.js applications need to read configuration files, write logs, serve uploads, save generated output, or load templates. All of those tasks depend on reliable file handling.

These modules matter because hardcoded paths often break when code moves between macOS, Linux, and Windows. A path that works on one platform may fail on another if you manually concatenate strings.

They also matter for safety and maintainability. Using the right path helpers prevents subtle bugs such as extra slashes, missing separators, incorrect relative paths, and files being written to the wrong directory.

3. Basic Syntax or Core Idea

Here is the minimal idea: import the modules, build a path, and then use fs to read or write the file.

Import the modules

In modern JavaScript, use require() in CommonJS projects or import in ES modules. This example uses CommonJS because it is still common in Node.js documentation and tutorials.

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

The node: prefix makes it explicit that these are built-in Node.js modules.

Build a file path

Instead of concatenating strings manually, use path.join().

const filePath = path.join('data', 'notes.txt');

This creates a valid path for the current platform.

Read or write the file

Then pass that path to a file system function.

fs.writeFileSync(filePath, 'Hello, file system!', 'utf8');
const contents = fs.readFileSync(filePath, 'utf8');
console.log(contents);

This shows the basic pattern you will use repeatedly: create a path, then operate on it with fs.

4. Step-by-Step Examples

Example 1: Read a text file

This example reads a text file from a data folder and prints its contents.

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

const filePath = path.join('data', 'message.txt');
const text = fs.readFileSync(filePath, 'utf8');

console.log(text);

Use utf8 when you want a string instead of a raw buffer.

Example 2: Write a file

This example creates or replaces a file with new content.

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

const filePath = path.join('logs', 'app.log');
fs.writeFileSync(filePath, 'Application started\n', 'utf8');

If the logs directory does not exist, you must create it first or Node.js will throw an error.

Example 3: Create a directory if it does not exist

Before writing files into a folder, make sure the folder exists.

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

const dirPath = path.join('output');

if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}

This avoids the common mistake of writing a file into a folder that has not been created yet.

Example 4: Build an absolute path from the current file

Relative paths often depend on where you start the Node.js process. Use __dirname in CommonJS to anchor paths to the current file.

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

const filePath = path.join(__dirname, 'config', 'settings.json');
const json = fs.readFileSync(filePath, 'utf8');
console.log(JSON.parse(json));

This is much more reliable than assuming the process starts in the same folder as the file.

5. Practical Use Cases

These use cases are common in CLI tools, back-end APIs, build scripts, and automation utilities.

6. Common Mistakes

Mistake 1: Concatenating paths by hand

Many beginners join folders and filenames with string concatenation. That can produce broken paths when separators are missing or duplicated.

Problem: Manually building paths can create invalid strings like datafile.txt instead of data/file.txt, and it is not portable across operating systems.

const folder = 'data';
const name = 'file.txt';
const filePath = folder + name;

Fix: Use path.join() so Node.js inserts the correct separator automatically.

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

const folder = 'data';
const name = 'file.txt';
const filePath = path.join(folder, name);

The corrected version works because path.join() creates a proper path for the current platform.

Mistake 2: Reading a file with the wrong relative path

Relative paths are resolved from the current working directory, not always from the file where the code lives. That often leads to ENOENT: no such file or directory.

Problem: This code may work in one terminal location and fail in another because the process starts in a different directory.

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

const text = fs.readFileSync('config/settings.json', 'utf8');

Fix: Build the path from __dirname in CommonJS, or from import.meta.url in ES modules.

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

const text = fs.readFileSync(path.join(__dirname, 'config', 'settings.json'), 'utf8');

The fixed version is anchored to the script location, so it is less likely to break when the app starts from another folder.

Mistake 3: Writing to a directory that does not exist

Node.js will not create missing parent folders automatically when you write a file. If the directory is missing, the write fails.

Problem: Attempting to write to reports/monthly.txt before creating reports can trigger an ENOENT error.

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

fs.writeFileSync('reports/monthly.txt', 'Done', 'utf8');

Fix: Create the directory first, using fs.mkdirSync() with recursive: true if needed.

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

const dirPath = 'reports';
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}

fs.writeFileSync(path.join(dirPath, 'monthly.txt'), 'Done', 'utf8');

The corrected version ensures the parent folder exists before the file write happens.

7. Best Practices

Use path.join() for path assembly

Path strings should be created with path helpers, not manual separators. This keeps your code portable and easier to read.

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

const reportPath = path.join('reports', '2026', 'summary.txt');

This approach avoids separator bugs and makes nested paths obvious.

Use absolute or anchored paths for application files

When a file belongs to your project, build the path from the current module location rather than from the working directory.

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

const filePath = path.join(__dirname, 'data', 'items.json');
const items = JSON.parse(fs.readFileSync(filePath, 'utf8'));

Anchored paths make your code more predictable in scripts, tests, and deployments.

Prefer the asynchronous API for application servers

Synchronous file operations block the event loop. In a server, that can slow down other requests while Node.js waits on disk I/O.

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

async function loadConfig() {
const filePath = path.join(__dirname, 'config', 'app.json');
const text = await fs.readFile(filePath, 'utf8');
return JSON.parse(text);
}

Asynchronous file APIs help keep servers responsive under load.

8. Limitations and Edge Cases

Note: A valid-looking path can still fail at runtime if permissions are missing, the directory is read-only, or the file is locked by another process.

9. Practical Mini Project

In this mini project, we will build a small utility that saves a report into a reports folder and then reads it back.

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

const reportsDir = path.join(__dirname, 'reports');
const reportPath = path.join(reportsDir, 'weekly.txt');

if (!fs.existsSync(reportsDir)) {
fs.mkdirSync(reportsDir, { recursive: true });
}

const report = [
'Weekly Report',
'Tasks completed: 14',
'Bugs fixed: 3'
].join('\n');

fs.writeFileSync(reportPath, report, 'utf8');

const savedReport = fs.readFileSync(reportPath, 'utf8');

console.log(savedReport);

This script demonstrates the full workflow: create a safe path, ensure the folder exists, write a file, and read it back to verify the contents.

10. Key Points

11. Practice Exercise

Create a script that writes a file named notes.txt inside a notes folder, then reads the file and prints its contents.

Expected output:

Node.js file handling is useful.

Hint: Combine fs.mkdirSync(), fs.writeFileSync(), and fs.readFileSync() with paths from path.join().

Solution:

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

const folderPath = path.join(__dirname, 'notes');
const filePath = path.join(folderPath, 'notes.txt');

if (!fs.existsSync(folderPath)) {
fs.mkdirSync(folderPath, { recursive: true });
}

fs.writeFileSync(filePath, 'Node.js file handling is useful.', 'utf8');
const text = fs.readFileSync(filePath, 'utf8');

console.log(text);

This solution shows the standard pattern for safe file handling in Node.js: create the path with path, then perform the file operation with fs.

12. Final Summary

The fs and path modules are two of the most important built-in tools in Node.js. Use fs when you need to read, write, create, or inspect files and folders. Use path whenever you need to construct or analyze a file path safely.

Most file-related bugs come from manual path concatenation, incorrect relative paths, or trying to write into directories that do not exist yet. If you learn to combine path.join() with the right fs method, you can handle real-world file tasks with far fewer surprises.

Next, try the asynchronous fs.promises API and practice reading JSON configuration files from your own project.