Path and File System

By Syncfusion®, Inc. · Last updated

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.

path.join glues folder names with the right slash and cleans up dot-dot "school" "class8" ".." "marks.csv" path.join( ... )

school/marks.csv (Windows: school\marks.csv)

path.join adds the right slash for the computer and removes the folder before "..".

fs Fundamentals

fs has three styles for the same jobs.

StyleExampleWaits?Use it for
SyncreadFileSync(p)Blocks everythingStart-up code and small scripts
CallbackreadFile(p, cb)NoOlder code
Promiseawait readFile(p) from node:fs/promisesNoNew code, with async/await
  • 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.

Animation: new data is written to marks.json.tmp, then renamed over marks.json in one step new data marks.json.tmp marks.json 1. writeFile(tmp) 2. rename(tmp, real)

A crash during step 1 leaves the old marks.json safe

Write to a temporary file first. The rename swaps it in all at once.

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.
// 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:

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.
// 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:

notice.txt changed: change

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

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
"reports/" + name/ works everywhereUse path.joinBroken paths on Windows, or //
Paths relative to process.cwd()The folder never changesUse import.meta.dirnameFiles are not found when run from another folder
existsSync before readFileChecking first is saferTry the action and catch the errorA race condition between check and use
writeFile straight over important dataWrites are instantWrite a temp file, then renameA crash leaves a half-written file
Forgetting { recursive: true } on mkdirParents are made for youPass recursive: trueENOENT for the missing parent
Not closing a file handleNode.js closes it at onceClose it in finallyEMFILE: too many open files
readFile(p) and using the result as textIt returns textPass "utf8", or call .toString()You get a Buffer, not a string

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.