Skip to content
JavaScript Basics: Working with Paths in Node.js

JavaScript Basics: Working with Paths in Node.js

October 16, 2026

Use Node’s built-in node:path module to assemble paths and extract filenames. These examples run on Linux with Node.js v24.19.0. No packages are needed.

Set up a scratch directory

Run this in Bash, and keep using the same terminal:

You need Bash, a text editor, the mktemp, rm, and rmdir utilities, and Node.js on your PATH. Save source files as UTF-8; their .mjs extension selects ES modules.

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Join parts and resolve an absolute path

In your editor, save this as path-build.mjs in the scratch directory:

import assert from 'node:assert/strict';
import path from 'node:path';

const relative = path.join('reports', 'drafts', '..', 'summary.txt');
const absolute = path.resolve('/srv/demo', relative);
const reset = path.resolve('/srv/demo', '/archive', 'summary.txt');

assert.equal(relative, 'reports/summary.txt');
assert.equal(absolute, '/srv/demo/reports/summary.txt');
assert.equal(reset, '/archive/summary.txt');
assert.equal(path.join('', ''), '.');
assert.equal(path.resolve(''), process.cwd());

console.log(relative);
console.log(absolute);
console.log(reset);

Run:

node path-build.mjs

Output:

reports/summary.txt
/srv/demo/reports/summary.txt
/archive/summary.txt

join() combines and normalizes segments. resolve() works from right to left until it has an absolute path; otherwise it uses the current working directory. The explicit /srv/demo base keeps this output predictable. See the official join and resolve reference.

These are lexical operations on strings. They don’t create directories, check existence, or resolve symbolic links. Normalization alone is not a security check for user-supplied paths.

Inspect a filename

Save as path-parts.mjs:

import assert from 'node:assert/strict';
import path from 'node:path';

const file = '/srv/demo/archive.tar.gz';
const parts = path.parse(file);

assert.equal(path.basename(file), 'archive.tar.gz');
assert.equal(path.extname(file), '.gz');
assert.equal(path.extname('.config'), '');
assert.equal(path.extname('report.'), '.');
assert.equal(parts.root, '/');
assert.equal(parts.dir, '/srv/demo');
assert.equal(parts.base, 'archive.tar.gz');
assert.equal(parts.name, 'archive.tar');
assert.equal(parts.ext, '.gz');

console.log(`filename: ${path.basename(file)}`);
console.log(`extension: ${path.extname(file)}`);
console.log(`directory: ${parts.dir}`);
console.log(`name: ${parts.name}`);

Run:

node path-parts.mjs

Output:

filename: archive.tar.gz
extension: .gz
directory: /srv/demo
name: archive.tar

The extension is only the final suffix. A leading dot alone doesn’t make .config have an extension. parse() separates the root, directory, filename, name, and extension.

Choose the path convention explicitly

Save as path-platforms.mjs:

import assert from 'node:assert/strict';
import path from 'node:path';

const windowsFile = String.raw`C:\reports\summary.txt`;
assert.equal(path.win32.basename(windowsFile), 'summary.txt');
assert.equal(path.posix.basename(windowsFile), windowsFile);
assert.equal(path.posix.join('/reports', 'summary.txt'), '/reports/summary.txt');

console.log(path.win32.basename(windowsFile));
console.log(path.posix.basename(windowsFile));
console.log(path.posix.join('/reports', 'summary.txt'));

Run:

node path-platforms.mjs

Output:

summary.txt
C:\reports\summary.txt
/reports/summary.txt

Default behavior follows the host OS. For paths received from another system, select path.win32 or path.posix. On POSIX, backslashes are ordinary filename characters.

Clean up

Remove only the tutorial files, then remove the empty scratch directory:

rm -- path-build.mjs path-parts.mjs path-platforms.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Acknowledgements

This post was created with assistance from AI.

Last updated on