File paths look simple until your code runs on a different operating system. Windows writes C:\Users\ada\reports\march.csv with backslashes, while macOS and Linux write /home/ada/reports/march.csv with forward slashes. Code that glues paths together with + '/' + works on one machine and breaks on another.
Node's built-in path module solves this. It joins path segments with the right separator, turns relative paths into absolute ones, and pulls paths apart into folder, file name and extension. Every Node developer uses it constantly, especially alongside the fs module from the previous lesson.
The os module gives information about the computer the program is running on: the operating system, the user's home folder, the temporary folder, the number of CPU cores and the amount of memory. In this lesson you'll use both modules, understand relative and absolute paths properly, and meet the URL class, which handles web addresses and file:// URLs.
Joining and resolving paths
import path from 'node:path';
path.join('data', 'reports', 'march.csv'); // data/reports/march.csv (data\reports\march.csv on Windows)
path.join('/srv/app', '../logs', 'app.log'); // /srv/logs/app.log
path.resolve('data', 'march.csv'); // an absolute path from the working directorypath.joincombines segments with the correct separator and tidies up..and..path.resolvebuilds an absolute path, working from right to left until it has a complete path, and using the current working directory if needed.- An absolute path starts at the root of the drive (
/orC:\). A relative path, likedata/march.csv, depends on where the program was started. - To build a path next to your script, start from
import.meta.dirname:path.join(import.meta.dirname, 'templates', 'email.html').
Taking paths apart
| Function | '/srv/app/uploads/photo.final.jpg' gives |
|---|---|
path.basename(p) | 'photo.final.jpg' |
path.basename(p, '.jpg') | 'photo.final' |
path.dirname(p) | '/srv/app/uploads' |
path.extname(p) | '.jpg' (only the last extension) |
path.parse(p) | { root, dir, base, ext, name } all at once |
path.relative(from, to) gives the path from one location to another, and path.sep is the separator for the current system. If you need POSIX-style paths everywhere, for example in URLs, use path.posix.join.
The os module and URLs
import os from 'node:os';
os.platform(); // 'win32', 'darwin' or 'linux'
os.homedir(); // the user's home folder
os.tmpdir(); // a folder for temporary files
os.cpus().length; // number of logical CPU cores
os.totalmem(); // total memory in bytes
os.EOL; // '\r\n' on Windows, '\n' elsewhereThe global URL class parses web addresses into parts: new URL('https://shop.example/search?q=lamp').searchParams.get('q') gives 'lamp'. In ES modules, import.meta.url is a file:// URL; fileURLToPath from node:url converts such URLs to ordinary paths.
Keeping user paths inside a folder
path.basename is enough when you only want a file name. Sometimes you need to accept a path with subfolders, such as reports/2026/march.csv, while still refusing anything that escapes your base folder. Resolve the path against the base, then check where it ended up with path.relative:
import path from 'node:path';
const base = path.resolve('/srv/uploads');
function insideBase(userPath) {
const full = path.resolve(base, userPath);
const rel = path.relative(base, full);
const ok = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
return ok ? full : null;
}
insideBase('sub/b.txt'); // '/srv/uploads/sub/b.txt'
insideBase('../etc/passwd'); // null
insideBase('/etc/passwd'); // null, an absolute path replaces the base
insideBase('sub/../../x'); // nullThe last two cases show why a simple check for '..' in the string isn't enough: resolve treats an absolute segment as a fresh start, and .. can hide in the middle of a path. Checking the result catches every trick. On Windows, relative returns a path on another drive as absolute, which the isAbsolute test rejects.
Normalising paths and more from os
path.normalize tidies a path without making it absolute: path.normalize('/srv//app/./logs/../tmp/') gives '/srv/app/tmp/'. Doubled slashes, . and .. are cleaned up, but a trailing slash is kept. path.join normalises its result in the same way, so path.join('a/', '/b') is simply 'a/b'. Note the difference from resolve: path.resolve('/a', '/b', 'c') gives '/b/c', because each absolute segment starts again from the root.
The os module has a few more functions you'll meet in real code:
| Function | Returns |
|---|---|
os.availableParallelism() | How many tasks the machine can run in parallel; a better choice than os.cpus().length for sizing worker pools |
os.hostname() | The machine's network name, useful in logs from several servers |
os.freemem() | Free memory in bytes |
os.uptime() | Seconds since the machine started |
os.arch() | The processor architecture, such as 'x64' or 'arm64' |
All of these describe the machine, not your program. For your own process's memory use, look at process.memoryUsage() instead.
Node.js Basics Syntax
path, os and URL
import path from 'node:path';
import os from 'node:os';
import { fileURLToPath } from 'node:url';
path.join(import.meta.dirname, 'public', 'index.html');
path.resolve('config.json');
path.basename('/a/b/c.txt'); // 'c.txt'
path.extname('archive.tar.gz'); // '.gz'
path.parse('/a/b/c.txt').name; // 'c'
path.relative('/a/b', '/a/c/d'); // '../c/d'
os.homedir(); os.tmpdir(); os.cpus().length;
new URL('https://x.dev/p?id=7').searchParams.get('id'); // '7'
fileURLToPath(import.meta.url);Node.js Basics Summary
- path.join combines segments with the right separator; path.resolve makes an absolute path.
- Relative paths depend on the working directory; build script-relative paths from import.meta.dirname.
- basename, dirname, extname and parse take paths apart.
- os.platform, homedir, tmpdir, cpus and totalmem describe the machine.
- URL parses web addresses; fileURLToPath converts file URLs to paths.
Node.js Basics Examples
Each example shows the code, the output it produces and what each part does.
Taking a path apart
import path from 'node:path';
const upload = '/srv/shop/uploads/2026/receipt.final.PDF';
const parts = path.posix.parse(upload);
console.log(parts.dir);
console.log(parts.base);
console.log(parts.name);
console.log(parts.ext.toLowerCase());
console.log(path.posix.basename(upload, parts.ext));
console.log(path.posix.join(parts.dir, '..', 'archive', parts.base));
console.log(path.posix.relative('/srv/shop', upload));/srv/shop/uploads/2026
receipt.final.PDF
receipt.final
.pdf
receipt.final
/srv/shop/uploads/archive/receipt.final.PDF
uploads/2026/receipt.final.PDFLine-by-line explanation
- Line 4path.posix uses forward slashes on every system, so this output is the same everywhere.
- Line 7base is the full file name; name drops the extension.
- Line 9The extension is lowercased before comparing, since users upload .PDF and .pdf.
- Line 11join resolves .. so the archive folder sits next to 2026.
- Line 12relative gives the path from one folder to a file.
Paths next to the script
import path from 'node:path';
import { writeFile, readFile, mkdir } from 'node:fs/promises';
const here = import.meta.dirname;
const templates = path.join(here, 'templates');
await mkdir(templates, { recursive: true });
await writeFile(path.join(templates, 'welcome.txt'), 'Welcome, {{name}}!');
const template = await readFile(path.join(templates, 'welcome.txt'), 'utf8');
console.log(template.replace('{{name}}', 'Ada'));
console.log(path.isAbsolute(here));
console.log(path.isAbsolute('templates/welcome.txt'));
console.log(path.basename(path.resolve('templates')) === 'templates');
const unsafeName = '../../etc/passwd';
console.log(path.basename(unsafeName));Welcome, Ada!
true
false
true
passwdLine-by-line explanation
- Line 4import.meta.dirname is the folder containing this script, whatever folder node was started from.
- Line 5join adds the right separator for the operating system.
- Lines 12 to 13isAbsolute tells absolute and relative paths apart.
- Line 14resolve turns a relative path into an absolute one using the working directory.
- Line 17basename strips folders from untrusted names, preventing writes outside the upload folder.
System information and URLs
import os from 'node:os';
const info = {
platformKnown: ['win32', 'darwin', 'linux'].includes(os.platform()),
hasHome: os.homedir().length > 0,
cores: os.cpus().length >= 1,
eol: JSON.stringify(os.EOL) === '"\\n"' || JSON.stringify(os.EOL) === '"\\r\\n"',
};
console.log(info);
const link = new URL('https://shop.example/search?q=desk+lamp&page=2#results');
console.log(link.hostname);
console.log(link.pathname);
console.log(link.searchParams.get('q'));
console.log(Number(link.searchParams.get('page')) + 1);
link.searchParams.set('page', '3');
console.log(link.href);{ platformKnown: true, hasHome: true, cores: true, eol: true }
shop.example
/search
desk lamp
3
https://shop.example/search?q=desk+lamp&page=3#resultsLine-by-line explanation
- Lines 3 to 8The values differ on every machine, so this example prints checks rather than raw values.
- Line 7os.EOL is the line ending: \n on macOS and Linux, \r\n on Windows.
- Line 11URL splits an address into hostname, pathname, search parameters and more.
- Line 14searchParams decodes + as a space.
- Lines 16 to 17Changing a parameter updates the whole URL.
Common Node.js Basics Mistakes
Joining paths with +
const file = folder + '/' + name; // wrong separator on Windows, breaks with trailing slashesconst file = path.join(folder, name);Assuming the working directory is the script's folder
const config = await readFile('config.json', 'utf8'); // fails when run from another folderconst config = await readFile(path.join(import.meta.dirname, 'config.json'), 'utf8');Trusting a file name from a user
await writeFile(path.join('uploads', req.body.name), data); // "../server.js" overwrites your codeawait writeFile(path.join('uploads', path.basename(req.body.name)), data);Node.js Basics in Real Projects
path and os in practice
Every Express app that serves files uses path.join(import.meta.dirname, 'public'). CLI tools store settings in path.join(os.homedir(), '.mytool'), write temporary files to os.tmpdir(), and use os.cpus().length to decide how many worker threads to start. Upload handlers sanitise names with path.basename and check extensions with path.extname before saving files.
Node.js Basics FAQ
What is the difference between path.join and path.resolve in Node.js?
path.join glues segments together and tidies the result, so path.join('a', 'b') stays relative as 'a/b'. path.resolve always returns an absolute path: it works from right to left, restarts whenever it meets an absolute segment, and falls back to the current working directory. Use join to build a path from known parts, and resolve when you need a full path to compare or store.
How do I get the directory of the current file in an ES module?
Use import.meta.dirname for the folder and import.meta.filename for the full file path; both are available from Node 20.11. On older versions, use path.dirname(fileURLToPath(import.meta.url)) with fileURLToPath from node:url. In CommonJS files, the old __dirname and __filename variables do the same job.
Why does path.extname return only .gz for archive.tar.gz?
extname returns everything from the last dot to the end, so double extensions are seen as a single .gz. If you need to recognise .tar.gz, check the end of the name yourself with name.toLowerCase().endsWith('.tar.gz'). A file name that starts with a dot and has no other dot, such as .env, has no extension at all: path.extname('.env') is ''.
Can I use forward slashes in paths on Windows?
Yes. Windows accepts / as well as \ in file paths, so readFile('data/users.json') works there too. The problem comes when you compare, split or display paths, because Node's own functions return backslashes on Windows. Building paths with path.join keeps everything consistent on every system.
How do I find the user's home directory or temp folder in Node.js?
Call os.homedir() for the home folder and os.tmpdir() for the system's temporary folder. Combine them with path.join, for example path.join(os.homedir(), '.myapp', 'config.json'). For temporary folders that need a unique name, mkdtemp(path.join(os.tmpdir(), 'myapp-')) from node:fs/promises creates one with random characters added.
Node.js Basics Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line to combine the folder and file name.
const file = path.('data', 'users.json');
What does this print?
console.log(path.extname('photo.backup.png'));
What's the difference between path.join and path.resolve?
What does this print?
console.log(path.posix.join('/app/src', '..', 'public', 'index.html'));
Why use path.basename on a file name submitted by a user?
Node.js Basics Quiz
5 questions to check what you learned.
Which module helps build file paths?
- fs
- path
- os
- url
What does os.homedir() return?
- The project folder
- The temporary folder
- The current user's home folder
- The Node install folder
What does path.dirname('/a/b/c.txt') return?
- c.txt
- /a
- /a/b
- .txt
Which value is the separator for the current operating system?
- os.EOL
- path.delimiter
- os.sep
- path.sep
What does new URL('https://a.dev/x?q=1').searchParams.get('q') return?
- 1 (a number)
- '1' (a string)
- null
- q=1