url / URLSearchParams
The node:url module gives you the same URL + URLSearchParams API the browser has, plus extras: fileURLToPath for ESM __dirname, format/resolve for legacy code, and domainToASCII for IDN. Reach for it any time you build, parse, or normalise URLs.
URL, searchParams, fileURLToPath, format
EXAMPLE
import { URL, URLSearchParams, fileURLToPath, pathToFileURL, domainToASCII } from 'node:url';
import path from 'node:path';
// 1) Parse a URL — same API as the browser
const u = new URL('https://user:pw@example.com:8443/path/to/x?q=1&q=2&page=3#section');
u.protocol; // 'https:'
u.username; // 'user'
u.password; // 'pw'
u.hostname; // 'example.com'
u.host; // 'example.com:8443'
u.port; // '8443'
u.pathname; // '/path/to/x'
u.search; // '?q=1&q=2&page=3'
u.searchParams; // URLSearchParams instance
u.hash; // '#section'
u.origin; // 'https://example.com:8443'
// 2) Build a URL — much safer than string concat
const api = new URL('/v1/users', 'https://api.example.com');
api.searchParams.set('limit', 50);
api.searchParams.append('tag', 'admin');
api.searchParams.append('tag', 'staff');
api.toString(); // 'https://api.example.com/v1/users?limit=50&tag=admin&tag=staff'
// 3) URLSearchParams — also constructible directly
const p = new URLSearchParams({ limit: 10, page: 2 });
p.set('q', 'hello world');
p.toString(); // 'limit=10&page=2&q=hello+world'
p.get('q'); // 'hello world'
p.getAll('tag'); // [] (array form)
p.has('q'); // true
p.delete('q');
for (const [k, v] of p) { /* iterate */ }
// Parse a query string into an object
Object.fromEntries(new URLSearchParams('a=1&b=2')); // { a: '1', b: '2' }
// 4) Resolve a relative URL
new URL('users/42', 'https://api.example.com/v1/').toString();
// 'https://api.example.com/v1/users/42'
new URL('../', 'https://api.example.com/v1/users/').toString();
// 'https://api.example.com/v1/'
new URL('?reset', api).toString(); // keeps base, replaces query
// 5) Validate / safely allowlist schemes
function safeUrl(input, base) {
try {
const u = new URL(input, base);
if (!['http:', 'https:'].includes(u.protocol)) return null;
return u.toString();
} catch { return null; }
}
safeUrl('https://example.com'); // valid
safeUrl('javascript:alert(1)'); // null
safeUrl('not a url'); // null
// 6) ESM __dirname + __filename
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const staticDir = path.join(__dirname, 'public');
// pathToFileURL — opposite direction
const fileUrl = pathToFileURL('/etc/hosts'); // file:///etc/hosts
// 7) IDN domains
domainToASCII('münchen.de'); // 'xn--mnchen-3ya.de' (Punycode)
// The URL constructor handles IDN automatically; this exposes the conversion if you need it.
// 8) Legacy url.format / url.parse — AVOID for new code
// The legacy 'url' API (url.parse, url.format) parsed differently than the WHATWG URL spec.
// Use new URL() everywhere in modern Node.
// 9) Encoding pitfalls
encodeURIComponent('hello world'); // 'hello%20world'
encodeURI('https://example.com/a b'); // 'https://example.com/a%20b'
// URLSearchParams encodes spaces as '+' — but new URL() can read either.
// 10) Building external API URLs in HTTP clients
const response = await fetch(api);
// Most HTTP libraries accept URL instances directly — pass new URL() over string concat.
// 11) Detecting if a URL is reachable from your server (SSRF safety)
function isInternalUrl(input) {
const u = new URL(input);
return ['localhost', '127.0.0.1', '0.0.0.0', '::1'].includes(u.hostname)
|| u.hostname.endsWith('.internal')
|| u.hostname.match(/^10\\.|^172\\.(1[6-9]|2\\d|3[01])\\.|^192\\.168\\./);
}
// Block internal URLs from server-side fetches. Resolve DNS in your fetch wrapper to be thorough.
// 12) Modifying a URL immutably
function withParam(input, key, value) {
const u = new URL(input);
u.searchParams.set(key, value);
return u.toString();
}
withParam('https://example.com?a=1', 'b', '2');
// 'https://example.com/?a=1&b=2'
// 13) Comparing URLs
function sameOrigin(a, b) {
return new URL(a).origin === new URL(b).origin;
}
// 14) Common bugs
// • Concatenating URLs with template literals — encoding wrong, double slashes, broken on Windows paths
// • Treating new URL() as throwing on bad protocol — only throws on UNPARSEABLE input; allowlist schemes
// • URLSearchParams keys are case-sensitive — be consistent
// • path-style queries (matrix params /a;k=v) — not supported by URL; parse manually
// • Forgetting to round-trip through URL when comparing — '/foo' vs '/foo/' differ
// • IPv6 hostnames need brackets: 'http://[::1]:3000'
// • String concatenation for query strings — always use URLSearchParams
Why it matters
Use new URL() + URLSearchParams for every URL operation: parse, build, normalise, validate. Recover ESM __dirname with fileURLToPath(import.meta.url), allowlist URL schemes for safety, and block private-IP destinations on server-side fetches to neutralise SSRF.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
const u = new URL('https://example.com/p?q=hi');
u.searchParams.get('q'); // 'hi'
u.pathname = '/other';
Try it Yourself »
Discussion
Loading…