path & URL Modules

Node.js · Lesson 8 of 30 7 min read Updated

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

JavaScript
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));
Output
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".

JavaScript
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);
Output
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:

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

Output
C:\node-course
C:\node-course\data\users.json
false
public\css\site.css
FunctionReturnsExample result
join(a, b)Pieces joined and tidieduploads/2026
resolve(a, b)Absolute path from the current folder/srv/app/uploads/2026
relative(from, to)The way from one path to anotherpublic/css/site.css
normalize(p)p with .., . and double slashes cleaned/srv/app/data
isAbsolute(p)true or falsefalse

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:

JavaScript
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);
}
Output
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:

JavaScript
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"));
Output
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:

JavaScript
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);
Output
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 + "/" +. Use path.join(); it handles separators and extra slashes.
  • Confusing join and resolve. join keeps relative paths relative; resolve always 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") > 500 compares strings; convert with Number() first.
  • Calling new URL("/path") without a base. A relative URL needs a base as the second argument.

Key points

  • Use node:path so paths work on Windows (\) and macOS/Linux (/).
  • basename, extname, dirname and parse split a path; join combines pieces; resolve gives an absolute path.
  • Block path traversal by resolving the path and checking it starts with the allowed folder plus path.sep.
  • The global URL class splits addresses into parts, and searchParams reads and builds query strings with correct encoding.
  • URL.canParse() and URL.parse() test addresses without exceptions; avoid the legacy url.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
JavaScript
// 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);
Output
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?

2. Which function always returns an absolute path?

3. new URL("https://x.in/s?max=500").searchParams.get("max") returns what?

4. Why add path.sep in full.startsWith(PUBLIC_DIR + path.sep)?

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