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 pass base.
  • base — optional. When present and url is only a path, the result is resolved relative to base.

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:

https:
protocol
//
site.com:8080
host
/path/page
pathname
?p1=v1&p2=v2
search
#hash
hash
host = hostname (site.com) + “:” + port (8080)
A URL split into its component properties.

The full reference for the properties:

  • href is the complete URL string, identical to what url.toString() returns.
  • protocol includes the trailing colon : (so "https:", not "https").
  • host is hostname plus port; hostname and port are also available separately.
  • search is the query string, starting with ?.
  • hash is the fragment, starting with #.
  • origin is protocol + host (e.g. "https://site.com:8080").
  • If the URL carries HTTP authentication like http://login:[email protected], then username and password are 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 that name.
  • get(name) — read the first value for name (or null).
  • getAll(name) — read every value for name as 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.

?tag=js
.append(‘tag’,‘css’) →
?tag=js&tag=css
?tag=js
.set(‘tag’,‘css’) →
?tag=css
How set and append differ when a key already exists.

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.)

’ ’ (space)
→ UTF-8 0x20 →
%20
‘я’
→ UTF-8 0xD1 0x8F →
%D1%8F
Percent-encoding: a character becomes its UTF-8 bytes in %XX form.

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:

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:

  • encodeURI escapes only what is never legal anywhere in a URL (spaces, most non-Latin letters, and so on). It leaves the structural characters alone.
  • encodeURIComponent escapes all of that plus the delimiters #, $, &, +, ,, /, :, ;, =, ?, and @, because inside a component those characters are data, not structure.
encodeURI — the whole address
leaves : / ? = & # intact · escapes spaces, non-Latin
encodeURIComponent — one piece
escapes ALL of the above too, so the piece can’t break the URL
encodeURI preserves URL structure; encodeURIComponent escapes it too.

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:

Building / parsing
new URL(…) → .protocol .host
.pathname .search .hash
.searchParams.set/get/append
Raw-string encoding
whole URL → encodeURI
one piece → encodeURIComponent
(decode with the matching fn)
Choosing your tool for URL work.

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.