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

## Pro Summary

`fs` comes in three styles: sync, callback, and `node:fs/promises`. The async styles run on libuv's threadpool, which defaults to 4 threads shared with `dns.lookup()`, async crypto, and zlib, so heavy file I/O can starve unrelated work. `path` is pure string manipulation. It never touches the disk, it collapses `..` segments, and it does nothing to stop path traversal. In ES modules, anchor project files to `import.meta.dirname` and `import.meta.filename`; `__dirname` exists only in CommonJS.

```
import { readFile } from 'node:fs/promises';
import path from 'node:path';

const file = path.join(import.meta.dirname, 'data', 'config.json');
const config = JSON.parse(await readFile(file, 'utf8'));
```

Requires: `import.meta.dirname`/`import.meta.filename`: Node 20.11+ (no longer experimental as of 24.0). `fs.promises.glob`: added in Node 22.0, stable since 24.0. `fs.cp`: stable since 22.3.

Gotchas:

- `path.join('/a', '/b')` gives `/a/b`, but `path.resolve('/a', '/b')` gives `/b`. `resolve` starts over at every absolute segment and falls back to `process.cwd()` when none is given.
- `mkdir(dir, { recursive: true })` doesn't throw when the directory already exists (it resolves `undefined`), so an `existsSync` check before it is redundant and racy.
- `writeFile` truncates the target and rewrites it in place, so a crash mid-write leaves a partial file. For config or state files, write a temp file in the same directory and `rename()` it over the target.
- `path.extname` only looks at the last dot: `'.bashrc'` gives `''` and `'archive.tar.gz'` gives `'.gz'`, so it isn't a reliable file-type check on its own.
- `fs.rmdir(dir, { recursive: true })` only emits deprecation warning DEP0147 in Node 24, but Node 25 removed the `recursive` option. Use `fs.rm(dir, { recursive: true, force: true })`, which also tolerates a missing path.

Primary sources:

- [Node.js docs: File system](https://nodejs.org/api/fs.html)
- [Node.js docs: Path](https://nodejs.org/api/path.html)
- [Node.js docs: import.meta.dirname](https://nodejs.org/api/esm.html#importmetadirname)
- [Node.js deprecations: DEP0147](https://nodejs.org/api/deprecations.html#DEP0147)

Source: https://devdocs10.com/articles/javascript/js-node-fs-path  
By Murat Bo · Updated 2026-10-17 · DevDocs10 (https://devdocs10.com)

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 with Node 24.21.0):

```text
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()`.

**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`.

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

## Where AI Coding Assistants Get This Wrong

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