URL objects
A web address looks simple from the outside: type it, press Enter, a page loads. Underneath, that string carries structure — a protocol, a host, a path, a query, a fragment — and each part has its own rules about which characters are legal. Manipulating URLs by hand with string concatenation and + signs is where bugs breed.
The built-in URL class gives you a proper interface for building and taking apart URLs. It knows the grammar so you don’t have to.
One thing to set expectations first: no network API in the browser demands a URL object. fetch, XMLHttpRequest, and friends all accept plain strings. So strictly speaking you never have to reach for URL. But once you’re editing query parameters or stitching paths together, doing it with a real object is far less error-prone.
Creating a URL
Here’s the constructor signature:
new URL(url, [base])
Two arguments:
url— the full URL, or just a path when you also passbase.base— optional. When present andurlis only a path, the result is resolved relative tobase.
The straightforward case is a complete address:
let url = new URL('https://shop.example/catalog/boots');
The two forms below produce identical URLs. In the second, the path and the origin are supplied separately and joined for you:
let url1 = new URL('https://shop.example/catalog/boots');
let url2 = new URL('/catalog/boots', 'https://shop.example');
alert(url1); // https://shop.example/catalog/boots
alert(url2); // https://shop.example/catalog/boots
The base argument doesn’t have to be a string. You can pass an existing URL object, which is handy when you want a new address relative to one you already have:
let url = new URL('https://shop.example/catalog/boots');
let newUrl = new URL('sandals', url);
alert(newUrl); // https://shop.example/catalog/sandals
Notice what happened there. The base ended at /catalog/boots, and sandals replaced the last segment — you got /catalog/sandals, not /catalog/boots/sandals. That’s standard relative-path resolution, the same logic a browser uses when a page links to a sibling file.
Because a URL object parses its input up front, its parts are available immediately as properties. That makes it a clean parser, not just a builder:
let url = new URL('https://shop.example/catalog');
alert(url.protocol); // https:
alert(url.host); // shop.example
alert(url.pathname); // /catalog
The anatomy of a URL
Every URL object exposes named pieces of the address. Here’s how a full URL maps onto those properties:
The full reference for the properties:
hrefis the complete URL string, identical to whaturl.toString()returns.protocolincludes the trailing colon:(so"https:", not"https").hostis hostname plus port;hostnameandportare also available separately.searchis the query string, starting with?.hashis the fragment, starting with#.originis protocol + host (e.g."https://site.com:8080").- If the URL carries HTTP authentication like
http://login:password@site.com, thenusernameandpasswordare populated too. This form is rare and discouraged, but the properties exist.
Search params “?…”
Say you want https://shop.example/find?query=sneakers. You could bake the query straight into the string:
new URL('https://shop.example/find?query=sneakers')
That works — until a value contains a space, an ampersand, a non-Latin letter, or any other character with special meaning in a URL. Then you have to encode it yourself, and it’s easy to get wrong.
The URL object hands you a dedicated tool: url.searchParams, a live URLSearchParams object tied to the URL’s query string. Change it, and the URL’s search updates to match.
Its methods:
append(name, value)— add a parameter (does not replace an existing one).delete(name)— remove all parameters with thatname.get(name)— read the first value forname(ornull).getAll(name)— read every value fornameas an array. Repeated keys are legal:?user=Maya&user=Raj.has(name)— check whether the parameter exists.set(name, value)— set or replace, collapsing any duplicates to one.sort()— order the parameters by name; occasionally useful for cache keys.- It’s also iterable, like a
Map, yielding[name, value]pairs.
The payoff is automatic encoding on the way out and automatic decoding on the way in:
let url = new URL('https://shop.example/find');
url.searchParams.set('q', 'blue socks!'); // a value with a space and a "!"
alert(url); // https://shop.example/find?q=blue+socks%21
url.searchParams.set('sort', 'price:asc'); // a value with a colon ":"
// parameters get encoded for you
alert(url); // https://shop.example/find?q=blue+socks%21&sort=price%3Aasc
// iterating gives you the DECODED values back
for (let [name, value] of url.searchParams) {
alert(`${name}=${value}`); // q=blue socks! then sort=price:asc
}
The space became +, the ! became %21, the : became %3A — all without you thinking about it. And when you read the values back by iterating, they’re decoded to their original form. Encoding is a transport detail the object manages for you.
set vs. append
This distinction bites people. append always adds; set replaces every match with a single entry.
If you loop and append inside the loop without clearing first, you can silently pile up duplicate keys. When you mean “this parameter should have exactly one value,” use set.
Encoding
The rules for what belongs in a URL come from a standard, RFC 3986. It defines the allowed characters and, by exclusion, the ones that must be escaped.
Anything not allowed gets percent-encoded: the character’s UTF-8 bytes are written out as %XX hex pairs. A space becomes %20. (Inside a query string, a space may instead be written as + — a historical quirk carried over from HTML form submissions.)
The good news: URL objects do all of this for you. Supply your values raw and let the string conversion handle encoding:
// this example uses Cyrillic characters
let url = new URL('https://ru.wikipedia.org/wiki/Река');
url.searchParams.set('mark', 'я');
alert(url); // https://ru.wikipedia.org/wiki/%D0%A0%D0%B5%D0%BA%D0%B0?mark=%D1%8F
Both Река in the path and я in the parameter came out encoded. Each Cyrillic letter takes two bytes in UTF-8, so each shows up as two %.. groups — which is why the string grows so much.
Encoding strings by hand
Before the URL class existed, everyone assembled URLs as plain strings. That still works, and for a quick one-off a string is often shorter to write. The cost: when you go the string route, you own the encoding and decoding yourself.
JavaScript ships four global functions for it:
- encodeURI — encode a whole URL.
- decodeURI — reverse it.
- encodeURIComponent — encode a single component: one query value, a path segment, a hash.
- decodeURIComponent — reverse it.
The obvious question: when do you use which? Look back at the anatomy of a URL:
https://site.com:8080/path/page?p1=v1&p2=v2#hash
Characters like :, ?, =, &, and # are structural — they hold the URL together. In a complete URL they must stay literal. But inside a single value, those same characters would break the structure, so there they have to be escaped.
That’s exactly the split between the two functions:
encodeURIescapes only what is never legal anywhere in a URL (spaces, most non-Latin letters, and so on). It leaves the structural characters alone.encodeURIComponentescapes all of that plus the delimiters#,$,&,+,,,/,:,;,=,?, and@, because inside a component those characters are data, not structure.
For a whole URL, encodeURI is the fit:
// Cyrillic in the path
let url = encodeURI('http://site.example/мир');
alert(url); // http://site.example/%D0%BC%D0%B8%D1%80
For a single parameter value, reach for encodeURIComponent:
let topic = encodeURIComponent('Cats&Dogs');
let url = `https://shop.example/find?q=${topic}`;
alert(url); // https://shop.example/find?q=Cats%26Dogs
Watch what happens if you use the wrong one here:
let topic = encodeURI('Cats&Dogs');
let url = `https://shop.example/find?q=${topic}`;
alert(url); // https://shop.example/find?q=Cats&Dogs
encodeURI left the & alone, because & is a perfectly valid URL character at the top level — it separates parameters. But that’s the whole problem. The server now reads q=Cats followed by a second, garbage parameter Dogs. Your value got split in two.
Summary at a glance
Two mental buckets to walk away with:
.pathname .search .hash
.searchParams.set/get/append
one piece → encodeURIComponent
(decode with the matching fn)
When you’re working with structured data — adding a query param, reading a host, joining paths — let the URL object carry the rules. When you just need to escape one value into a string you’re building by hand, pick encodeURIComponent for a component and encodeURI for a full address.