path & URL Modules
Programs work with two kinds of addresses all the time: file paths such as uploads/2026/photo.jpg, and URLs such as https://shop.example.in/products?q=bat. Joining these by hand with + and slashes breaks easily, especially because Windows writes paths differently from macOS and Linux.
In this lesson you will use the built-in node:path module to take paths apart and join them safely, protect your server from a common attack called path traversal, and use the URL class to read and build web addresses.
Why not just join strings?
Windows separates folders with a backslash (C:\Users\Priya\notes.txt). macOS and Linux use a forward slash (/home/priya/notes.txt). If you write folder + "/" + file, your code may produce mixed or doubled slashes, or break on another system. Your laptop may run Windows while your server runs Linux, so the same code must handle both. The path module knows the rules of the system it is running on.
Taking a path apart
import path from "node:path";
const photo = "/home/meera/uploads/2026/diwali-rangoli.jpg";
console.log(path.basename(photo)); // file name
console.log(path.basename(photo, ".jpg")); // without extension
console.log(path.extname(photo)); // extension
console.log(path.dirname(photo)); // folder
console.log(path.parse(photo));diwali-rangoli.jpg
diwali-rangoli
.jpg
/home/meera/uploads/2026
{
root: '/',
dir: '/home/meera/uploads/2026',
base: 'diwali-rangoli.jpg',
ext: '.jpg',
name: 'diwali-rangoli'
}path.extname() is useful for checking file types, for example to accept only .jpg and .png uploads. path.parse() returns every part at once.
Joining and resolving paths
path.join() glues pieces together with the right separator and tidies the result. .. means "go up one folder" and . means "this folder".
import path from "node:path";
console.log(path.join("uploads", "2026", "photo.jpg"));
console.log(path.join("uploads/", "/2026/", "../2025", "photo.jpg"));
console.log(path.posix.join("uploads", "2026", "photo.jpg"));
console.log(path.win32.join("uploads", "2026", "photo.jpg"));
console.log(path.sep);uploads\2026\photo.jpg uploads\2025\photo.jpg uploads/2026/photo.jpg uploads\2026\photo.jpg \
This output is from Windows. On macOS or Linux the first two lines use / and path.sep is /. If you ever need one style on every system, path.posix always uses / and path.win32 always uses \.
path.resolve() is different: it always returns an absolute path, meaning a full path from the root of the drive. It starts from the current working directory and works through the pieces from left to right:
import path from "node:path";
console.log(process.cwd());
console.log(path.resolve("data", "users.json"));
console.log(path.isAbsolute("data/users.json"));
console.log(path.relative("C:\\node-course", "C:\\node-course\\public\\css\\site.css"));Run from the folder C:\node-course on Windows, it prints:
C:\node-course C:\node-course\data\users.json false public\css\site.css
| Function | Returns | Example result |
|---|---|---|
join(a, b) | Pieces joined and tidied | uploads/2026 |
resolve(a, b) | Absolute path from the current folder | /srv/app/uploads/2026 |
relative(from, to) | The way from one path to another | public/css/site.css |
normalize(p) | p with .., . and double slashes cleaned | /srv/app/data |
isAbsolute(p) | true or false | false |
Advanced: blocking path traversal
Imagine a route that sends files from a public folder, where the user chooses the file name. An attacker sends ../../secret/.env, and a careless server joins it to the folder and sends your passwords. This attack is called path traversal. Resolve the full path, then check that it is still inside the folder you allow:
import path from "node:path";
const PUBLIC_DIR = path.resolve("public");
function safePath(userInput) {
const full = path.resolve(PUBLIC_DIR, userInput);
if (!full.startsWith(PUBLIC_DIR + path.sep)) {
throw new Error(`Blocked: ${userInput}`);
}
return full;
}
console.log(path.relative(PUBLIC_DIR, safePath("images/logo.png")));
try {
safePath("../../secret/.env");
} catch (err) {
console.log(err.message);
}images\logo.png Blocked: ../../secret/.env
Adding path.sep matters: without it, a folder called public-backup would also pass the startsWith check.
The URL class
URL is a global class in Node.js, the same one browsers have. Give it a full address and it splits it into parts:
const link = new URL("https://shop.example.in:8443/products/search?q=cricket+bat&max=2500&brand=SG&brand=MRF#reviews");
console.log(link.protocol);
console.log(link.hostname);
console.log(link.port);
console.log(link.pathname);
console.log(link.search);
console.log(link.hash);
const params = link.searchParams;
console.log(params.get("q"));
console.log(Number(params.get("max")));
console.log(params.getAll("brand"));
console.log(params.has("sort"), params.get("sort"));https: shop.example.in 8443 /products/search ?q=cricket+bat&max=2500&brand=SG&brand=MRF #reviews cricket bat 2500 [ 'SG', 'MRF' ] false null
The part after ? is the query string. searchParams decodes it for you: + becomes a space, and repeated keys can be read with getAll(). Values are always strings, so convert numbers yourself, and a missing key gives null.
Building URLs safely
Never build URLs by joining strings with user input; a name like Rohtak & Hisar contains &, which would break the query string. Let URL and URLSearchParams encode values:
const api = new URL("/v1/trains", "https://api.example.in");
api.searchParams.set("from", "NDLS");
api.searchParams.set("to", "JP");
api.searchParams.set("date", "2026-11-14");
api.searchParams.append("class", "3A");
api.searchParams.append("class", "SL");
console.log(api.href);
const search = new URLSearchParams({ name: "Rohan Verma", city: "Rohtak & Hisar" });
console.log(search.toString());
console.log(URL.canParse("not a url"));
console.log(URL.parse("/relative/path"));
console.log(URL.parse("/relative/path", "https://example.in")?.href);https://api.example.in/v1/trains?from=NDLS&to=JP&date=2026-11-14&class=3A&class=SL name=Rohan+Verma&city=Rohtak+%26+Hisar false null https://example.in/relative/path
The second argument to new URL() is a base: a relative path is resolved against it. new URL("not a url") throws an error with code ERR_INVALID_URL. To test a string without try...catch, use URL.canParse(), or URL.parse(), which returns null instead of throwing (added in Node.js 22.1).
File URLs
ES modules identify files by URL, so import.meta.url looks like file:///C:/node-course/app.js. The node:url module converts between file URLs and paths with fileURLToPath() and pathToFileURL(). Many fs functions also accept a URL directly, as in readFile(new URL("./data.json", import.meta.url)).
Common mistakes
- Joining paths with
+ "/" +. Usepath.join(); it handles separators and extra slashes. - Confusing
joinandresolve.joinkeeps relative paths relative;resolvealways gives an absolute path from the current working directory. - Trusting user-supplied file names. Resolve them and check they stay inside the allowed folder.
- Forgetting that query values are strings.
params.get("max") > 500compares strings; convert withNumber()first. - Calling
new URL("/path")without a base. A relative URL needs a base as the second argument.
Key points
- Use
node:pathso paths work on Windows (\) and macOS/Linux (/). basename,extname,dirnameandparsesplit a path;joincombines pieces;resolvegives an absolute path.- Block path traversal by resolving the path and checking it starts with the allowed folder plus
path.sep. - The global
URLclass splits addresses into parts, andsearchParamsreads and builds query strings with correct encoding. URL.canParse()andURL.parse()test addresses without exceptions; avoid the legacyurl.parse().
Exercise
Write links.js. Given the address https://results.example.in/class-12/marks?roll=4521&board=CBSE&year=2026, print the host name, the path, and the roll number as a number. Then build a new URL for the same host with the path /class-10/marks and the query values roll=3310 and board=HBSE, and print its full address.
Show solution
// links.js
const result = new URL("https://results.example.in/class-12/marks?roll=4521&board=CBSE&year=2026");
console.log(result.hostname);
console.log(result.pathname);
console.log(Number(result.searchParams.get("roll")));
const next = new URL("/class-10/marks", result.origin);
next.searchParams.set("roll", "3310");
next.searchParams.set("board", "HBSE");
console.log(next.href);results.example.in /class-12/marks 4521 https://results.example.in/class-10/marks?roll=3310&board=HBSE
result.origin gives https://results.example.in, which works as the base for the new URL, and searchParams.set() adds the query values with correct encoding.
Quick Quiz
1. What does path.extname("report.final.pdf") return?
extname returns the extension from the last dot to the end.
2. Which function always returns an absolute path?
resolve works from the current working directory and returns a full path.
3. new URL("https://x.in/s?max=500").searchParams.get("max") returns what?
Query values are always strings; convert them with Number() when you need numbers.
4. Why add path.sep in full.startsWith(PUBLIC_DIR + path.sep)?
Without the separator, any path that merely starts with the same letters would be allowed.
Learn Node.js with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
