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.

  • fs handles file content and directory operations.
  • path handles path strings such as /home/user/file.txt or C:\Users\file.txt.
  • They are both built into Node.js, so you do not need to install anything.
  • They are often used together: path creates the path, and fs reads or writes the file at that path.

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

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.

JavaScript
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().

JavaScript
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.

JavaScript
fs.writeFileSync(filePath, 'Hello, file system!', 'utf8');
const contents = fs.readFileSync(filePath, 'utf8');
console.log(contents);
Output✓ Tested · Node 24
Uncaught ReferenceError: fs is not defined

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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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

  • Reading environment-specific configuration files from disk.
  • Writing log files during development or production debugging.
  • Loading templates, fixtures, or seed data.
  • Saving generated reports, exports, or images.
  • Managing uploads and temporary files in server applications.
  • Checking whether a file or folder exists before creating it.

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.

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

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

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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.

JavaScript
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

  • path.join() does not check whether the file exists; it only builds the string.
  • fs.readFileSync() and other sync methods block the event loop while they run.
  • fs paths can fail with ENOENT if the file or folder does not exist.
  • Windows and POSIX platforms use different separators, so manual slashes can be unreliable.
  • Reading a file without an encoding returns a Buffer, not a string.
  • path.normalize() cleans up redundant separators, but it does not make a path exist.

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.

JavaScript
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

  • fs works with file contents and directories.
  • path builds and normalizes path strings safely.
  • Use path.join() instead of manual string concatenation.
  • Use anchored paths when reading project files.
  • Create missing directories before writing files into them.
  • Prefer asynchronous file APIs for long-running applications.

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.

  • Use path.join() to build the folder and file path.
  • Create the folder if it does not already exist.
  • Write the text Node.js file handling is useful. into the file.
  • Read the file back with utf8 encoding.

Expected output:

JavaScript
Node.js file handling is useful.

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

Show solution
JavaScript
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.

Where AI Coding Assistants Get This Wrong

Patterns AI tools commonly produce for this topic, and what to use instead.

Often suggested: Uses __dirname or require inside an ES module, or pastes path.dirname(fileURLToPath(import.meta.url)) into every file.

Instead: Use import.meta.dirname and import.meta.filename (Node 20.11+). Keep the fileURLToPath form only for older runtimes.

__dirname is a variable from the CommonJS module wrapper, so it throws ReferenceError: __dirname is not defined in ESM.

Often suggested: Treats path.join(UPLOAD_DIR, req.params.name) or path.normalize() as input sanitization.

Instead: Resolve, then check containment: const root = path.resolve(UPLOAD_DIR); const target = path.resolve(root, name); if (!target.startsWith(root + path.sep)) throw .... Add fs.realpath if symlinks matter.

path.join('/srv/uploads', '../../etc/passwd') returns /etc/passwd, because join and normalize resolve .. rather than rejecting it.

Often suggested: Adds rimraf, mkdirp, glob, or fs-extra, or wraps calls in util.promisify(fs.readFile), for operations core Node already handles.

Instead: Use fs.rm({ recursive: true, force: true }), mkdir({ recursive: true }), fs.promises.glob, fs.cp, and the node:fs/promises API.

These are built in and stable in Node 24, so the extra packages only add supply-chain surface and API drift.

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.