URL and URLSearchParams
Parse and build URLs safely with the URL API — search params, base resolution, encoding, and common SPA routing helpers.
- javascript
- url-and
String-splitting query strings breaks on encoding edge cases. The URL and URLSearchParams APIs parse and serialize correctly — use them for links, fetch targets, and router glue.
const url = new URL('https://example.com:443/path?q=a%20b#hash');
url.protocol; // 'https:'
url.hostname; // 'example.com'
url.port; // '' (default port omitted)
url.pathname; // '/path'
url.search; // '?q=a%20b'
url.hash; // '#hash'
url.origin; // 'https://example.com'
Relative resolution
const abs = new URL('/api/users', 'https://example.com/app/');
// https://example.com/api/users
const abs2 = new URL('users', 'https://example.com/app/');
// https://example.com/app/users
new URL(input) without base throws if input isn’t absolute. Handy for validating user-entered absolute URLs:
function isAbsoluteHttpUrl(s) {
try {
const u = new URL(s);
return u.protocol === 'http:' || u.protocol === 'https:';
} catch {
return false;
}
}
Search params
const url = new URL('https://example.com/search?q=react&page=1');
url.searchParams.get('q'); // 'react'
url.searchParams.getAll('tag'); // multi
url.searchParams.set('page', '2');
url.searchParams.append('tag', 'js');
url.searchParams.delete('tag');
url.searchParams.has('q'); // true
url.toString(); // encoded properly
// Build from scratch
const params = new URLSearchParams({ q: 'a b', page: '1' });
params.toString(); // 'q=a+b&page=1' (application/x-www-form-urlencoded style)
fetch(`/api?${params}`);
Iterate:
for (const [k, v] of url.searchParams) {
console.log(k, v);
}
Encoding footguns
// Manual — easy to get wrong
// '?' + 'q=' + encodeURIComponent(q)
// URLSearchParams encodes values; note space → + in query form
// Path encoding is different — set pathname carefully
url.pathname = '/a b'; // browser normalizes/encodes
Don’t put raw unescaped user input into path segments without encoding (encodeURIComponent per segment).
SPA helpers
function updateQuery(patch) {
const url = new URL(window.location.href);
for (const [k, v] of Object.entries(patch)) {
if (v == null) url.searchParams.delete(k);
else url.searchParams.set(k, String(v));
}
window.history.replaceState(null, '', url);
}
Works with the History API for shareable filter state.
vs location properties
location.search is a string; parsing by hand is obsolete. document.URL is a string snapshot — prefer new URL(location.href).
Interview answer (out loud)
“I use the URL constructor with an optional base for resolution and validation. URLSearchParams handles get/set/append and correct encoding for query strings. I avoid manual split/join for queries and use history.replaceState with URL when syncing SPA filters.”
Auth and open redirects
function safeRedirect(next) {
const u = new URL(next, window.location.origin);
if (u.origin !== window.location.origin) {
return '/'; // reject open redirect
}
return u.pathname + u.search + u.hash;
}
Never trust a ?next= query as a free navigation target without origin checks.
Sorting and stable serialization
function stableQuery(params) {
const sp = new URLSearchParams(params);
const entries = [...sp.entries()].sort(([a], [b]) => a.localeCompare(b));
return new URLSearchParams(entries).toString();
}
Useful for cache keys so a=1&b=2 and b=2&a=1 hit the same entry when order shouldn’t matter.
Hash vs search
const url = new URL(location.href);
url.hash; // client-only; not sent to server on request
url.search; // sent on navigation/fetch to that URL
SPAs sometimes store UI state in the hash; prefer search for shareable filters that SSR or analytics should see — product decision.
file: and blob: URLs
const u = new URL('blob:https://example.com/uuid');
// still parseable; be careful comparing origins with blob:
Object URLs from URL.createObjectURL need revokeObjectURL — different topic, same URL namespace.
Further reading
Related
- History API for SPA Routing
- Fetch API Fundamentals
- Regular Expressions Essentials
- String Methods Worth Knowing
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.