Node.js path and os

Build file paths that work on Windows, macOS and Linux with the path module, take paths apart, resolve absolute paths, read system information with os, and work with URLs.

Lesson 5 of 43Beginner18 min read

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

JavaScript
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 directory
  • path.join combines segments with the correct separator and tidies up .. and ..
  • path.resolve builds 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 (/ or C:\). A relative path, like data/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

JavaScript
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' elsewhere

The 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:

JavaScript
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');    // null

The 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:

FunctionReturns
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.

Example 1

Taking a path apart

Node.js
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));
Output
/srv/shop/uploads/2026
receipt.final.PDF
receipt.final
.pdf
receipt.final
/srv/shop/uploads/archive/receipt.final.PDF
uploads/2026/receipt.final.PDF
Line-by-line explanation
  1. Line 4path.posix uses forward slashes on every system, so this output is the same everywhere.
  2. Line 7base is the full file name; name drops the extension.
  3. Line 9The extension is lowercased before comparing, since users upload .PDF and .pdf.
  4. Line 11join resolves .. so the archive folder sits next to 2026.
  5. Line 12relative gives the path from one folder to a file.
Example 2

Paths next to the script

Node.js
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));
Output
Welcome, Ada!
true
false
true
passwd
Line-by-line explanation
  1. Line 4import.meta.dirname is the folder containing this script, whatever folder node was started from.
  2. Line 5join adds the right separator for the operating system.
  3. Lines 12 to 13isAbsolute tells absolute and relative paths apart.
  4. Line 14resolve turns a relative path into an absolute one using the working directory.
  5. Line 17basename strips folders from untrusted names, preventing writes outside the upload folder.
Example 3

System information and URLs

Node.js
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);
Output
{ platformKnown: true, hasHome: true, cores: true, eol: true }
shop.example
/search
desk lamp
3
https://shop.example/search?q=desk+lamp&page=3#results
Line-by-line explanation
  1. Lines 3 to 8The values differ on every machine, so this example prints checks rather than raw values.
  2. Line 7os.EOL is the line ending: \n on macOS and Linux, \r\n on Windows.
  3. Line 11URL splits an address into hostname, pathname, search parameters and more.
  4. Line 14searchParams decodes + as a space.
  5. Lines 16 to 17Changing a parameter updates the whole URL.

Common Node.js Basics Mistakes

Joining paths with +

Don't
const file = folder + '/' + name;              // wrong separator on Windows, breaks with trailing slashes
Do
const file = path.join(folder, name);

Assuming the working directory is the script's folder

Don't
const config = await readFile('config.json', 'utf8');   // fails when run from another folder
Do
const config = await readFile(path.join(import.meta.dirname, 'config.json'), 'utf8');

Trusting a file name from a user

Don't
await writeFile(path.join('uploads', req.body.name), data);   // "../server.js" overwrites your code
Do
await 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.

Fill in the blankseasy

Complete the line to combine the folder and file name.

const file = path.('data', 'users.json');
Predict the outputeasy

What does this print?

console.log(path.extname('photo.backup.png'));
Multiple choicemedium

What's the difference between path.join and path.resolve?

Predict the outputmedium

What does this print?

console.log(path.posix.join('/app/src', '..', 'public', 'index.html'));
Multiple choicehard

Why use path.basename on a file name submitted by a user?

Node.js Basics Quiz

5 questions to check what you learned.

  1. Which module helps build file paths?

    • fs
    • path
    • os
    • url
  2. What does os.homedir() return?

    • The project folder
    • The temporary folder
    • The current user's home folder
    • The Node install folder
  3. What does path.dirname('/a/b/c.txt') return?

    • c.txt
    • /a
    • /a/b
    • .txt
  4. Which value is the separator for the current operating system?

    • os.EOL
    • path.delimiter
    • os.sep
    • path.sep
  5. What does new URL('https://a.dev/x?q=1').searchParams.get('q') return?

    • 1 (a number)
    • '1' (a string)
    • null
    • q=1