Concatenating paths with '/' works on macOS and Linux. It breaks on Windows. That’s why the path module exists — it gives us platform-agnostic path operations so our code runs the same everywhere.
In simple language: never write dir + '/' + file. Always use path.join(dir, file). The module figures out the right separator for the OS we’re running on.
path.join vs path.resolve
These two trip everyone up. The difference matters.
path.join— just glues segments together with the OS separator. Relative stays relative.path.resolve— produces an absolute path, walking from right to left until it hits an absolute segment (or falling back to the current working directory).
import path from 'node:path';
path.join('foo', 'bar', 'baz.txt');
// 'foo/bar/baz.txt' (still relative)
path.resolve('foo', 'bar', 'baz.txt');
// '/Users/manish/proj/foo/bar/baz.txt' (absolute, from cwd)
path.resolve('/etc', 'config', '../app.conf');
// '/etc/app.conf' (absolute segment wins, .. collapsed)
Rule of thumb: use resolve when we need an absolute path (passing to fs, comparing paths). Use join for building a relative subpath.
The useful helpers
path.dirname('/var/log/app.log'); // '/var/log'
path.basename('/var/log/app.log'); // 'app.log'
path.extname('/var/log/app.log'); // '.log'
path.parse('/var/log/app.log');
// { root: '/', dir: '/var/log', base: 'app.log', name: 'app', ext: '.log' }
path.parse is great when we need multiple pieces at once.
The ESM __dirname problem
CommonJS had __dirname and __filename baked in as globals. ESM ("type": "module" in package.json) doesn’t. When we switch to ESM, those globals disappear and code breaks.
In simple language: ESM modules don’t know their own location for free anymore — we have to compute it from import.meta.url, which is a file:// URL.
__filename
+ fileURLToPath
The workaround:
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// now use as before
const config = path.join(__dirname, 'config.json');
Why fileURLToPath? Because import.meta.url is a string like file:///Users/manish/app/index.js — we have to convert that URL into a regular filesystem path before passing to fs.
Newer Node (20.11+) actually gives us import.meta.dirname and import.meta.filename directly, which skips the dance. Use them when our Node version allows.
URL parsing
Node uses the WHATWG URL standard (same as browsers). Forget the old url.parse — it’s deprecated.
const u = new URL('https://api.example.com/v1/users?id=42&active=true');
u.hostname; // 'api.example.com'
u.pathname; // '/v1/users'
u.searchParams.get('id'); // '42'
u.searchParams.get('active'); // 'true'
searchParams is a URLSearchParams object — iterable, supports append, delete, set. Way nicer than parsing query strings by hand.
Building URLs is just as clean:
const u = new URL('https://api.example.com');
u.pathname = '/v1/users';
u.searchParams.set('id', '42');
u.toString(); // 'https://api.example.com/v1/users?id=42'
The mental model
Use path.join for relative paths, path.resolve for absolute. Convert import.meta.url whenever you need __dirname in ESM. Parse URLs with new URL(), never string-splitting. These three habits cover most real-world cases without ever shipping a Windows-broken bug.