# Path and File System

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: System and Files
Source: https://www.devtutorials.ai/learn/nodejs/system-files/path-and-file-system

> Building file paths with the path module (join, resolve, normalize, relative, basename, dirname, extname, parse, format, isAbsolute, sep and delimiter), Windows and POSIX paths, fileURLToPath and import.meta.dirname, the sync, callback and promise fs APIs, reading text, JSON and folders, writing and appending, file flags, safe writes, copying, renaming and deleting, mkdir and mkdtemp, checking existence without race conditions, stat and permissions, symlinks, low-level file handles and watching files.

## Path and File System

**File system** = the files and folders on a computer's disk.

- **`node:path`** builds file paths, such as `reports/class8/asha.txt`.
- **`node:fs`** reads, writes and organises files and folders.
- Together, they let Node.js do the paper work of an office: save, find, copy and clean up.

**Simple lesson:** `path` makes the address, and `fs` visits it.

### A Real-World Example

Think of the school's record room.

- Every class has a cupboard. Every cupboard has files.
- To find Asha's file, you need its address: "Cupboard 8, shelf 2, file Asha".
- The clerk then opens it, reads it, adds a page or makes a copy.
- A careful clerk never tears out the old page before the new one is ready.

`path` writes the address. `fs` is the clerk.

## What You Will Learn

In this lesson, you will learn:

1. The `path` module and its main functions
2. Windows and POSIX paths, and file URLs
3. The three styles of the `fs` API, and why `fs/promises` is the usual choice
4. Reading text, JSON and folders
5. Writing, appending, file flags and safe writes
6. Copying, renaming, deleting and making folders
7. Checking existence without race conditions
8. `stat`, permissions, symlinks and file handles
9. Watching files for changes

## The path Module

- **`join(...parts)`** = glue parts with the right separator and clean up `..` and `.`.
- **`resolve(...parts)`** = build an **absolute** path, starting from the current folder.
- **`normalize(p)`** = clean a messy path.
- **`relative(from, to)`** = the path from one place to another.
- **`basename(p)`**, **`dirname(p)`**, **`extname(p)`** = the file name, the folder and the extension.
- **`parse(p)`** and **`format(obj)`** = split a path into parts, and build it back.
- **`isAbsolute(p)`** = does the path start from the root?
- **`sep`** and **`delimiter`** = the folder separator (`/`) and the list separator (`:`).

### Windows vs POSIX Paths

- **POSIX** (Mac and Linux) uses `/`, such as `/home/asha/report.txt`.
- **Windows** uses `\`, such as `C:\Users\Asha\report.txt`.
- `path` uses the style of the computer it runs on.
- `path.win32` and `path.posix` force one style.
- Never glue paths with `+ "/" +`. Always use `path.join`.

`path.join` also cleans up `..` and doubled slashes for you.

### fileURLToPath and import.meta.dirname

- An ES module knows its own address as a URL: `import.meta.url` = `file:///school/app.js`.
- **`fileURLToPath(url)`** = turn a `file://` URL into a normal path. **`pathToFileURL(path)`** goes the other way.
- **`import.meta.dirname`** = the folder of the current file, directly.
- Build data paths from the module's folder, not from `process.cwd()`. Then the program works from any folder.

**Simple lesson:** always build paths with `path.join`, starting from `import.meta.dirname`.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 170" role="img" ariaLabelledby="fig-node-pathjoin" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-pathjoin">path.join glues folder names with the right slash and cleans up dot-dot</title>
<rect x="20" y="20" width="110" height="36" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/><text x="75" y="43" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>"school"</text>
<rect x="145" y="20" width="110" height="36" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/><text x="200" y="43" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>"class8"</text>
<rect x="270" y="20" width="90" height="36" rx="8"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)" }}/><text x="315" y="43" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>".."</text>
<rect x="375" y="20" width="165" height="36" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)" }}/><text x="457" y="43" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>"marks.csv"</text>
<text x="295" y="90" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>path.join( ... )</text>
<path d="M280 64 L280 104"  style={{ stroke: "var(--ngc-purple)", strokeWidth: 2 }}/><path d="M273 98 L280 108 L287 98"  style={{ fill: "none", stroke: "var(--ngc-purple)", strokeWidth: 2 }}/>
<rect x="90" y="114" width="380" height="40" rx="10" opacity="0" style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}><animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.35;0.45;1" dur="5s" repeatCount="indefinite" /></rect>
<text x="280" y="139" textAnchor="middle" opacity="0" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>school/marks.csv   (Windows: school\marks.csv)<animate attributeName="opacity" values="0;0;1;1" keyTimes="0;0.35;0.45;1" dur="5s" repeatCount="indefinite" /></text>
</svg>
<figcaption>path.join adds the right slash for the computer and removes the folder before "..".</figcaption>
</figure>

## fs Fundamentals

`fs` has three styles for the same jobs.

<div className="tscroll">

| Style | Example | Waits? | Use it for |
|---|---|---|---|
| Sync | `readFileSync(p)` | Blocks everything | Start-up code and small scripts |
| Callback | `readFile(p, cb)` | No | Older code |
| Promise | `await readFile(p)` from `node:fs/promises` | No | New code, with `async`/`await` |

</div>

- Choose **`node:fs/promises`** for new code. It reads cleanly and never blocks.
- Wrap calls in `try`/`catch` and check `err.code`, as in **Error Handling**.

Sync calls like `readFileSync` are fine in small scripts and at start-up, before a server starts taking requests.

**Simple lesson:** use `fs/promises` with `await` unless you have a good reason not to.

## Reading Files and Folders

- **`readFile(p, "utf8")`** = the whole file as text.
- **`readFile(p)`** with no encoding = the raw bytes, as a `Buffer`.
- **Reading JSON** = `JSON.parse(await readFile(p, "utf8"))`.
- **`readdir(dir)`** = the names inside a folder.
- **`readdir(dir, { withFileTypes: true })`** = entries that know if they are files or folders.
- **`readdir(dir, { recursive: true })`** = names from all sub-folders too.

**Simple lesson:** read text with `"utf8"`, parse JSON yourself, and use `withFileTypes` to sort files from folders.

## Writing Files

- **`writeFile(p, data)`** = make or replace a file.
- **`appendFile(p, data)`** = add to the end of a file.
- **File flags** = how a file is opened:
  - `"w"` write (the default for `writeFile`), `"a"` append, `"wx"` write only if the file does not exist yet
- A missing folder gives `ENOENT`. Make it first with `mkdir`.

### Safe Writes: Write Temp, Then Rename

- If the program crashes in the middle of `writeFile`, the file may be half written.
- **Safe write** = write to a temporary file first, then rename it over the real one.
- A rename is all-or-nothing on the same disk. Readers see the old file or the new one, never half.

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 170" role="img" ariaLabelledby="fig-node-safewrite" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-safewrite">Animation: new data is written to marks.json.tmp, then renamed over marks.json in one step</title>
<rect x="30" y="40" width="150" height="50" rx="10"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="105" y="70" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>new data</text>
<rect x="215" y="40" width="150" height="50" rx="10"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}/><text x="290" y="70" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>marks.json.tmp</text>
<rect x="400" y="40" width="140" height="50" rx="10"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="470" y="70" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>marks.json</text>
<text x="200" y="125" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>1. writeFile(tmp)</text>
<text x="385" y="125" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 12 }}>2. rename(tmp, real)</text>
<circle r="7" cx="180" cy="65" style={{ fill: "var(--ngc-blue)" }}><animate attributeName="cx" values="180;215;215;365;400;400" keyTimes="0;0.2;0.45;0.55;0.75;1" dur="5s" repeatCount="indefinite" /></circle>
<text x="280" y="155" textAnchor="middle" style={{ fill: "var(--muted)", fontFamily: "var(--sans)", fontSize: 12 }}>A crash during step 1 leaves the old marks.json safe</text>
</svg>
<figcaption>Write to a temporary file first. The rename swaps it in all at once.</figcaption>
</figure>

**Simple lesson:** `writeFile` replaces, `appendFile` adds, and write-then-rename keeps data safe.

## Managing Files and Folders

- **`mkdir(dir, { recursive: true })`** = make a folder and any missing parents. No error if it exists.
- **`mkdtemp(prefix)`** = make a new, uniquely named temporary folder.
- **`copyFile(a, b)`** = copy one file. **`cp(a, b, { recursive: true })`** = copy a whole folder.
- **`rename(a, b)`** = move or rename.
- **`unlink(p)`** = delete one file. **`rm(p, { recursive: true, force: true })`** = delete a folder and everything in it.

### Checking Existence and Avoiding Race Conditions

**Race condition** = the result depends on timing, because something changed between two steps.

- Bad: `if (exists(file)) { read(file) }`. Another program may delete the file between the two lines.
- Good: just **try** the action and handle the error.
- `access(p)` is fine for a quick yes or no in a report. Do not use it as a lock before acting.

`rm` with `recursive: true` deletes a whole folder at once, with no Recycle Bin. Check the path first.

**Simple lesson:** use `recursive` for folders, and try the action instead of checking first.

## Metadata and Permissions

- **`stat(p)`** = facts about a file: size, dates and type.
- **`isFile()`** and **`isDirectory()`** = what kind of entry it is.
- **`size`** = bytes. **`mtime`** = last time the content changed. **`birthtime`** = when it was made.
- **`lstat(p)`** = like `stat`, but describes a symlink itself instead of its target.
- **`chmod(p, 0o644)`** = change who may read and write (Mac and Linux). **`chown`** = change the owner.
- **Symlink** = a shortcut file that points to another path. **`realpath(p)`** = the real path behind it.

```js
// main.mjs: permissions and symlinks on Linux
import { writeFile, chmod, stat, symlink, realpath, lstat } from "node:fs/promises";

await writeFile("secret.txt", "exam answers");
await chmod("secret.txt", 0o600); // only the owner may read and write
console.log("Mode:", ((await stat("secret.txt")).mode & 0o777).toString(8));

await symlink("secret.txt", "shortcut.txt");
console.log("Is the shortcut a symlink?", (await lstat("shortcut.txt")).isSymbolicLink());
console.log("Real file:", (await realpath("shortcut.txt")).split("/").pop());
```

Output:

```text
Mode: 600
Is the shortcut a symlink? true
Real file: secret.txt
```

**Simple lesson:** `stat` tells you size, type and dates. `chmod` controls who may read.

## Low-Level Access (Advanced)

**File descriptor** = a small number the operating system gives to an open file.

- `open(p, flags)` from `fs/promises` returns a **FileHandle**, an object around the descriptor.
- Use it to do many reads or writes on one open file.
- Always **close** it, or the computer runs out of descriptors (`EMFILE`).
- `try`/`finally` makes sure `close()` runs.

## Watching

- **`fs.watch(p, fn)`** = get an event when a file or folder changes. Fast, uses the operating system.
- **`fs.watchFile(p, fn)`** = checks the file every few seconds. Slower, but works on network drives.
- **Limits:** events can come twice, file names may be missing, and editors save files in surprising ways.
- For real projects, wait a short time after an event before acting, or use `node --watch` for restarts.

```js
// main.mjs
import { watch } from "node:fs";
import { writeFile } from "node:fs/promises";

await writeFile("notice.txt", "old notice");
const watcher = watch("notice.txt", (eventType) => {
  console.log("notice.txt changed:", eventType);
  watcher.close();
});
setTimeout(() => writeFile("notice.txt", "new notice"), 50);
```

Output:

```text
notice.txt changed: change
```

**Simple lesson:** watching works, but expect extra or missing events and handle them calmly.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| `"reports/" + name` | `/` works everywhere | Use `path.join` | Broken paths on Windows, or `//` |
| Paths relative to `process.cwd()` | The folder never changes | Use `import.meta.dirname` | Files are not found when run from another folder |
| `existsSync` before `readFile` | Checking first is safer | Try the action and catch the error | A race condition between check and use |
| `writeFile` straight over important data | Writes are instant | Write a temp file, then rename | A crash leaves a half-written file |
| Forgetting `{ recursive: true }` on `mkdir` | Parents are made for you | Pass `recursive: true` | `ENOENT` for the missing parent |
| Not closing a file handle | Node.js closes it at once | Close it in `finally` | `EMFILE: too many open files` |
| `readFile(p)` and using the result as text | It returns text | Pass `"utf8"`, or call `.toString()` | You get a `Buffer`, not a string |

</div>

## TL;DR

1. **`path.join`** = glue parts safely. **`path.resolve`** = make it absolute.
2. **`basename`, `dirname`, `extname`, `parse`** = take a path apart.
3. **`import.meta.dirname`** = the folder of this module. Build paths from it.
4. **`fs/promises`** = the usual choice. Use `await` and `try`/`catch`.
5. **Read** = `readFile(p, "utf8")`, `readdir(dir, { withFileTypes: true })`.
6. **Write** = `writeFile`, `appendFile`, flag `"wx"` for new files only.
7. **Safe write** = write a temp file, then `rename`.
8. **Do not check, then act** = try the action and handle the error.
9. **`stat`** = size, type and dates. **FileHandle** = always close it.

## Check Yourself

## Exercises

## What to Remember

- Build every path with `path.join`, starting from `import.meta.dirname`.
- Use `fs/promises` with `await` and `try`/`catch`.
- Pass `"utf8"` to get text instead of a Buffer.
- Make folders with `mkdir(dir, { recursive: true })`.
- Save important files with write-temp-then-rename.
- Try the action and handle the error, instead of checking first.
- Close every file handle you open.

Next: **Buffers, Streams and Compression**. You will handle raw bytes and process big files a piece at a time.
