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:pathbuilds file paths, such asreports/class8/asha.txt.node:fsreads, 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:
- The
pathmodule and its main functions - Windows and POSIX paths, and file URLs
- The three styles of the
fsAPI, and whyfs/promisesis the usual choice - Reading text, JSON and folders
- Writing, appending, file flags and safe writes
- Copying, renaming, deleting and making folders
- Checking existence without race conditions
stat, permissions, symlinks and file handles- 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)andformat(obj)= split a path into parts, and build it back.isAbsolute(p)= does the path start from the root?sepanddelimiter= 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 asC:\Users\Asha\report.txt. pathuses the style of the computer it runs on.path.win32andpath.posixforce one style.- Never glue paths with
+ "/" +. Always usepath.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 afile://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.
| 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 |
- Choose
node:fs/promisesfor new code. It reads cleanly and never blocks. - Wrap calls in
try/catchand checkerr.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 aBuffer.- 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 forwriteFile),"a"append,"wx"write only if the file does not exist yet
- A missing folder gives
ENOENT. Make it first withmkdir.
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()andisDirectory()= what kind of entry it is.size= bytes.mtime= last time the content changed.birthtime= when it was made.lstat(p)= likestat, 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)fromfs/promisesreturns 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/finallymakes sureclose()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 --watchfor 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
| 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 |
TL;DR
path.join= glue parts safely.path.resolve= make it absolute.basename,dirname,extname,parse= take a path apart.import.meta.dirname= the folder of this module. Build paths from it.fs/promises= the usual choice. Useawaitandtry/catch.- Read =
readFile(p, "utf8"),readdir(dir, { withFileTypes: true }). - Write =
writeFile,appendFile, flag"wx"for new files only. - Safe write = write a temp file, then
rename. - Do not check, then act = try the action and handle the error.
stat= size, type and dates. FileHandle = always close it.
Check Yourself
Exercises
What to Remember
- Build every path with
path.join, starting fromimport.meta.dirname. - Use
fs/promiseswithawaitandtry/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.