The path Module
node:path builds and dissects filesystem paths correctly — handling the differences between operating systems so you never hard-code a / or \ again. It is pure string manipulation: path never touches the disk. That makes it fast, predictable, and the right tool for every "where is this file?" question.
Why not just concatenate strings?
Because separators differ across platforms, and gluing strings invites bugs — double slashes, missing slashes, wrong direction. path.join normalizes all of it:
import path from 'node:path'
// FRAGILE — wrong separator on Windows, easy to double-up slashes
const bad = 'src' + '/' + 'lib' + '/' + 'index.js'
// ROBUST — correct on every OS, slashes normalized
const good = path.join('src', 'lib', 'index.js')
// → 'src/lib/index.js' (POSIX) or 'src\lib\index.js' (Windows)The essential functions
Function | Does | Example → result |
|---|---|---|
| Join + normalize segments |
|
| Resolve to an absolute path |
|
| Last segment |
|
| Containing directory |
|
| File extension |
|
| Break into an object |
|
| Reassemble from parts | inverse of |
| Path from one to another |
|
| Absolute or not |
|
join vs resolve — the key distinction
Both combine segments, but they differ in one crucial way: resolve always produces an absolute path (walking right-to-left until it has one, falling back to the cwd), while join just glues and normalizes:
path.join('/foo', 'bar', 'baz') // → '/foo/bar/baz'
path.join('/foo', '../bar') // → '/bar' (normalizes '..')
path.resolve('/foo', 'bar') // → '/foo/bar'
path.resolve('foo', 'bar') // → '/current/working/dir/foo/bar'
path.resolve('/foo', '/bar') // → '/bar' (absolute arg resets the base)Anchor paths to the file, not the cwd
The robust pattern
// CommonJS
const path = require('node:path')
const configPath = path.join(__dirname, 'config.json')
// ES Modules (Node 20.11+)
import path from 'node:path'
const configPath2 = path.join(import.meta.dirname, 'config.json')Why __dirname and import.meta.dirname behave this way is explained in The Module Wrapper Function.
Cross-platform: posix and win32
By default path uses the conventions of the current OS. When you need to force one — say, parsing a Windows path on a Linux CI server — use the explicit sub-namespaces:
path.win32.join('C:\Users', 'app') // → 'C:\Users\app' (always Windows-style)
path.posix.join('/usr', 'local') // → '/usr/local' (always POSIX-style)
console.log(path.sep) // '/' on POSIX, '\' on Windows
console.log(path.delimiter) // ':' on POSIX, ';' on Windows (PATH separator)Dissecting a path with parse
console.log(path.parse('/home/user/report.final.pdf')){
root: '/',
dir: '/home/user',
base: 'report.final.pdf',
ext: '.pdf',
name: 'report.final'
}