URL & URLSearchParams
Learn to parse, build, encode, decode, validate, and safely edit URLs with URL and URLSearchParams instead of fragile string concatenation.
- 01Read every part of a URLUse
new URL(input, base)to read the protocol, host, path, query string, and hash. - 02Build query strings safelyUse
url.searchParamsmethods to add filters, page numbers, and repeated names. - 03Pick the right toolKnow when to use
encodeURI,encodeURIComponent, the decoders,URL.canParse, andURL.parse.
A URL is an address with parts
A URL is the address of a page or file on the web, like https://example.com/docs?q=url. It looks like one piece of text. But it is made of parts:
- The protocol, like
https:, says how to fetch the page. - The host, like
example.com, names the computer (the server) that has the page. - The port, like
:8443, is a door number on that computer. Most URLs leave it out. - The path, like
/docs/url, says which page on that computer. - The query string, like
?q=url, carries extra data asname=valuepairs. - The hash, like
#examples, points to one spot inside the page.
JavaScript has two built-in tools for these parts:
URLreads a whole address and splits it into its parts.URLSearchParamsreads and changes the query string, the part after?.
Why not just join strings with +? Joining works for plain words. It breaks when the text has special characters. Say a user searches for rock & roll. In a URL, & means "a new pair starts here". So the search gets cut in two. Browsers and Node.js already know all the URL rules. These two tools use those rules for you.
new URL(input, base?) reads an address and splits it into parts you can read and change. The ? after base means you may leave it out. url.searchParams is a URLSearchParams object for the query string. It is linked to the URL: change one, and the other changes too.
An address on an envelope is one block of writing. But the post office reads it in parts: the city, the street, the door. A URL works the same way. The punctuation marks, like ://, /, ?, and #, show where one part ends and the next part begins.
- In real life: Post or courier
- In JavaScript: protocol, like
https: - In real life: City and street
- In JavaScript: host, like
example.com - In real life: Flat door number
- In JavaScript: port, like
:8443 - In real life: Room inside the flat
- In JavaScript: path (
pathnamein code), like/docs/url - In real life: A note with questions
- In JavaScript: query string (
searchin code), like?q=url - In real life: A sticky tab on one page of a book
- In JavaScript: hash, like
#examples
If strings are still new to you, keep the Strings and Unicode and strings lessons open. This lesson also turns query pairs into an object. The Object.keys, values and entries lesson explains how that works.
Split an address with new URL()
INTERACTIVETo parse text means to read it and split it into its parts. new URL() parses an address. You can call it in two ways:
new URL(input)needs a full address that starts with a protocol, likehttps://example.com/docs. A full address is called an absolute URL.new URL(input, base)also takes a short address, like../search. A short address is called a relative URL. JavaScript works out the full address from thebase, the full address you pass second.
If the input is not a valid URL, new URL() stops and throws a TypeError. It does not guess.
The new URL object has one property for each part. It also tidies some parts, so the same address always looks the same:
hrefis the whole address.originis the protocol, host, and port together, likehttps://example.com:8443. It answers "which site is this?"portis an empty string,"", when the URL uses the usual port for its protocol. Forhttps:, the usual port is 443.hostnamewrites a name with non-English letters, likemañana.example, in plain letters, digits, and dashes that start withxn--. This form is called punycode.hashkeeps its#. Only the browser uses the hash. It is not sent to the server.
const url = new URL("https://example.com:8443/docs/page?tab=api#examples");
console.log(url.protocol);
console.log(url.host);
console.log(url.pathname);
console.log(url.search);
console.log(url.hash);
console.log(url.origin);Line 1 parses one absolute URL. Lines 2 to 7 print six of its parts:
- Line 2 prints the protocol,
https:. - Line 3 prints the host,
example.com:8443.hostincludes the port.hostnamewould leave the port out. - Line 4 prints the path,
/docs/page. - Line 5 prints the query string,
?tab=api. It keeps its?. - Line 6 prints the hash,
#examples. - Line 7 prints the origin,
https://example.com:8443.
new URL() splits one address into named parts. Each card is the value of one property, such as url.host.const base = "https://example.com/docs/guides/";
const url = new URL("../search?q=rock", base);
console.log(url.href);The base ends in /guides/. The input ../search means "go up one folder, then find search". So JavaScript leaves guides/ and stays in /docs/. Line 3 prints https://example.com/docs/search?q=rock.
An input that starts with / works differently. It starts from the top of the site. So /search with the same base gives https://example.com/search.
const url = new URL(input, base);console.log(url.href);console.log(url.protocol);console.log(url.hostname);console.log(url.searchParams.getAll("tag"));- href
https://example.com:8443/docs/page?tab=api#examples- origin
https://example.com:8443- protocol
https:- username
(empty)- password
(empty)- host
example.com:8443- hostname
example.com- port
8443- pathname
/docs/page- search
?tab=api- hash
#examples- tag values
(none)
This is a valid URL. URL.canParse gives true. Checking for URL.parse in your browser….
Try the "Unicode domain" preset. The hostname row shows the punycode form. In href, the letters é and í in the path and hash turn into codes like %C3%A9. This is called percent-encoding; a later section explains it.
Then try the "Username + password" preset. A URL can hold a username and a password before the @. Real apps should not show them or write them to logs, because anyone who sees the URL would see the password.
Build a query string with URLSearchParams
INTERACTIVEThe query string is the part after ?. It is a list of name=value pairs, joined by &. For example, ?q=boots&page=2 has two pairs: q is boots, and page is 2.
The same name can appear more than once. Shops often do this for filters: ?tag=js&tag=web.
On a paper form, you don't write & and = marks yourself. You just fill in labeled boxes. URLSearchParams works like that form. You give it names and values. It writes the query string for you, and it writes special characters in a safe form.
- In real life: Writing in the box labeled Search
- In JavaScript:
set('q', value) - In real life: Ticking several topics
- In JavaScript:
append('tag', value), called more than once - In real life: Rubbing out a box
- In JavaScript:
delete('page') - In real life: Putting the boxes in A to Z order
- In JavaScript:
sort()
Every URL object has a searchParams property. It is linked to the URL. When you change searchParams, url.search and url.href change right away. These are its main methods:
set(name, value)puts in one value. It replaces any old values with that name.append(name, value)adds one more value. Old values with that name stay.delete(name)removes every pair with that name.has(name)returnstrueif a pair with that name is there.get(name)returns the first value for that name, ornullif there is none.getAll(name)returns every value for that name, as an array.sort()puts the pairs in order by name.toString()writes the query string, without the?.
You can also loop over it with for...of. Each step of the loop gives one [name, value] pair.
const params = new URLSearchParams("tag=js&tag=web&page=2");
params.set("page", "3");
params.append("tag", "url");
console.log(params.get("tag"));
console.log(params.getAll("tag"));
console.log(params.has("page"));
params.delete("page");
params.sort();
console.log(params.toString());- Line 1 makes
paramsfrom a query string. It has twotagvalues,jsandweb, and a page,2. - Line 2 uses
setto replace page2with3. - Line 3 uses
appendto add a third tag,url. The two old tags stay. - Line 4 uses
get, so it prints only the first tag:js. - Line 5 uses
getAll, so it prints all three:[ 'js', 'web', 'url' ]. - Line 6 prints
true, becausepageis still there. - Line 7 removes
page. Line 8 sorts the pairs by name. Onlytagpairs are left, andsortkeeps pairs with the same name in their old order. - Line 9 prints
tag=js&tag=web&tag=url.
params holds three tag pairs and one page pair. get gives only the first tag; getAll gives every tag.const url = new URL("https://example.com/search");url.searchParams.set("q", query);for (const tag of tags) url.searchParams.append("tag", tag);url.searchParams.set("page", page);url.searchParams.sort();console.log(url.href);console.log([...url.searchParams]);- url.href
https://example.com/search?q=rock+%26+roll+%F0%9F%8E%B8&tag=music&tag=live+shows&page=2- toString()
q=rock+%26+roll+%F0%9F%8E%B8&tag=music&tag=live+shows&page=2- getAll("tag")
["music","live shows"]- has("page")
true- pairs from a loop
[["q","rock & roll 🎸"],["tag","music"],["tag","live shows"],["page","2"]]- new URLSearchParams(object)
q=rock+%26+roll+%F0%9F%8E%B8&page=2- Object.fromEntries
{"q":"rock & roll 🎸","tag":"live shows","page":"2"}
The URL now: https://example.com/search?q=rock+%26+roll+%F0%9F%8E%B8&tag=music&tag=live+shows&page=2
Look at the spaces in the lab's URL. URLSearchParams writes each space as +. Web forms have always sent spaces this way, so query strings follow the same rule. The rule has a long name: application/x-www-form-urlencoded. When you read a value back through URLSearchParams, each + turns back into a space.
Encode special characters, then decode them
INTERACTIVESome characters have a job in a URL:
&separates one pair from the next.=separates a name from its value.?starts the query string.#starts the hash./separates the folders in the path.
Sometimes these characters are part of the data, like the & in rock & roll. Then they must be written in a safe form, so they are not read as URL punctuation.
That safe form is called percent-encoding. Each special character becomes % and a code. For example, & becomes %26, and a space becomes %20. Some characters, like emoji, need several codes. To encode text means to write it in this safe form. To decode it means to turn it back into normal text.
A shipping clerk reads every label on a parcel. If a loose item inside has writing on it, the clerk may read that writing as part of the address. So you wrap the item. Encoding wraps a special character as a percent code, such as %26, so nobody reads it as punctuation.
- In real life: Bubble-wrapping a glass
- In JavaScript: percent-encoding a space,
&,=,?,#, or emoji - In real life: Leaving the address label unwrapped
- In JavaScript:
encodeURIleaves URL punctuation as it is - In real life: Wrapping one item all over
- In JavaScript:
encodeURIComponentencodes every special character in one value
encodeURIComponent(value);encodeURI(value);new URLSearchParams({ q: value }).toString();decodeURIComponent("%E0%A4%A"); // throws URIError- encodeURIComponent
rock%20%26%20roll%3F%20yes%2F%231%20%E2%98%95- encodeURI
rock%20&%20roll?%20yes/#1%20%E2%98%95- URLSearchParams
q=rock+%26+roll%3F+yes%2F%231+%E2%98%95- decodeURIComponent, back again
rock & roll? yes/#1 ☕- decodeURI, back again
rock & roll? yes/#1 ☕- + read by URLSearchParams
rock and roll- + read by decodeURIComponent
rock+and+roll- broken %E0%A4%A
URIError
encodeURI leaves URL punctuation like ?, &, =, /, and # as it is. encodeURIComponent encodes them. URLSearchParams writes each space as +.
The table lists the tools. Each row says what one tool is for and what it does.
| Tool | Use it for | What it does |
|---|---|---|
encodeURIComponent(value) | One piece of data: one query value, one folder name in a path, or one hash value. | It encodes URL punctuation such as ?, &, =, /, and #, so they count as data. |
encodeURI(url) | A whole URL that already has its punctuation. | It leaves URL punctuation as it is: ; , / ? : @ & = + $ #. It encodes spaces and other characters. |
URLSearchParams | A whole query string. | It writes each space as + and encodes the rest. Reading a value back through it decodes the value. |
decodeURI / decodeURIComponent | Turning encoded text back into normal text. | Both throw a URIError if a percent code is broken, like %E0%A4%A. The encoders throw URIError too, if the text has half of an emoji on its own. |
Here is the difference on one folder name, reports/2026. The slash is part of the name, not a split between two folders.
encodeURI leaves the slash alone, so it still splits the path. encodeURIComponent turns it into %2F, so the name stays in one piece.- One query value: rock & roll
- A whole URL with a space in the path: https://example.com/a b?x=1
- One folder name in a path that contains /
- Building a query from
{ q, page } - Filters like
tag=js&tag=web - A hash value typed by a user: intro#part
Sort each task by the tool you would use first.
Check a URL without errors, then build API URLs safely
STEP THROUGHnew URL() throws an error for a bad address. Sometimes you just want a yes or no answer. For example, a form may want to check what a user typed. Two tools help. They are static methods: you call them on URL itself, not on a URL object.
URL.canParse(input, base?)returnstrueorfalse. It never throws.URL.parse(input, base?)returns a URL object, ornullif the input is not valid. It never throws either.
Both tools are new. URL.parse is the newest, so some older browsers don't have it yet. Check that it exists before you use it, for example with typeof URL.parse === "function". This check is called feature detection.
If URL.parse is missing, write a small helper. Inside it, call new URL() inside try/catch. If it throws, return null. Now your code can check if (url === null) instead of catching an error.
Next is the bug these tools help you avoid. An API here is an address your code calls to get data from a server. The code below builds an API address in two ways:
- Line 2 joins the search text onto the address with
+. - Lines 3 to 5 use
URLandsearchParams.
Joining works for a plain word. It breaks when the search text has an &.
Pick a search term. Guess which URL survives special characters. Then step through the code.
script
const unsafe = base + "?q=" + "rock & roll";const url = new URL(base);url.searchParams.set("q", "rock & roll");url.searchParams.append("tag", "music & culture");console.log(unsafe);console.log(url.href);The safe version treats the search text as data. set and append encode each value. So the server gets exactly what the user typed, and exactly the tag you added.
& starts a new pair and cuts the search in two. set encodes it as %26, so the server gets one value.Where you’ll use this
Your code will meet URLs in many places: menu links, share buttons, page numbers, sign-in pages that send you back where you were, and every lesson that fetches data from a server. The steps are almost always the same:
- Start from a base address you trust.
- Add path or query parts with the URL tools, not with
+. - Read
hrefat the end.
Some places you'll do this:
- Search pages: start from
new URL(location.href).location.hrefis the address of the current page. ChangesearchParams, then show the new address in the address bar. - API calls: start from the API's base address. Append filters and page numbers. Then pass
url.hreftofetch, the function that asks a server for data. - Return addresses: a sign-in page often gets a "go back to" URL. Check it with
URL.canParse. Then compare itsoriginwith your own site before you send the user there. - Links you build: encode each changing folder name with
encodeURIComponentif it might contain/.
Marketing links often carry labels in the query string. utm_source says where the click came from. utm_campaign names the campaign.
const url = new URL("https://example.com/news?utm_source=email&utm_campaign=fall");
console.log(url.searchParams.get("utm_source"));
console.log(url.searchParams.get("utm_campaign"));Line 1 parses the full link once. Line 2 reads utm_source and prints email. Line 3 reads utm_campaign and prints fall. If a name is missing, get returns null. That is easier than cutting the raw text at each &.
function safeLink(input, base) {
if (!URL.canParse(input, base)) return null;
const url = new URL(input, base);
return url.protocol === "https:" ? url.href : null;
}
console.log(safeLink("https://example.com/help", "https://app.example.com/"));
console.log(safeLink("javascript:alert(1)", "https://app.example.com/"));- Line 2 returns
nullifURL.canParsesays the text is not a URL at all. - Line 4 returns the link only if its protocol is
https:. Otherwise it returnsnull. - Line 6 passes an https link, so it prints
https://example.com/help. - Line 7 passes
javascript:alert(1). That is a valid URL, but clicking it runs code. Its protocol isjavascript:, so line 4 returnsnull.
For a real return address, also compare origin with a list of sites you allow. Checking only that the text starts with https:// doesn't tell you which site the user will land on.
Common misconceptions
- “A URL is just a string.” You can show a URL as a string. But it has parts and rules.
new URL()knows these rules. String methods likesplitdon't. - “The hash goes to the server.” The browser keeps the hash. It uses it to jump to a spot in the page. Requests to the server don't include it.
- “
encodeURIandencodeURIComponentdo the same thing.”encodeURIis for a whole address, so it leaves punctuation like/and?alone.encodeURIComponentis for one piece of data, so it encodes them. - “A plus sign always means a space.” Only in query strings, which follow the web form rule.
URLSearchParamsuses that rule.decodeURIComponent("+")returns+, not a space. - “Turning params into an object keeps repeated names.”
Object.fromEntries(new URLSearchParams("tag=js&tag=web"))keeps only the lasttag,web. An object can't have two properties with the same name. UsegetAllto get every value.
Practice: URL objects
5 EXERCISESRead the starter code. What does the first console.log print?
const url = new URL("https://example.com:443/docs?q=hi#top");
console.log(url.origin);
console.log(url.port);
console.log(url.hash);The first log prints https://example.com. The URL had :443. But 443 is the usual port for https:, so origin leaves it out.
What does the second console.log print?
const url = new URL("https://example.com:443/docs?q=hi#top");
console.log(url.origin);
console.log(url.port);
console.log(url.hash);The second log prints an empty string, "", so you see a blank line. :443 is the usual port for https:. The third log prints #top.
Run the code or work it out. What does the tag part of the URL look like?
const url = new URL("https://shop.example/search");
url.searchParams.set("q", "red shoes");
url.searchParams.append("tag", "sale & outlet");
console.log(url.href);The URL contains tag=sale+%26+outlet. Each space became +, and the & became %26. So the whole phrase stays one tag value.
A teammate writes "/search?q=" + search. Explain why it breaks for rock & roll. Then rewrite it with URL.
const search = "rock & roll";
const url = new URL("https://example.com/search");
url.searchParams.set("q", search);
console.log(url.href);The bug is '/search?q=' + search. The & in the search text looks like the start of a new pair. searchParams.set writes it as q=rock+%26+roll, so the whole search stays together.
What does Object.fromEntries(params).tag keep?
const params = new URLSearchParams("tag=js&tag=web&page=2");
console.log(params.getAll("tag").join(","));
console.log(Object.fromEntries(params).tag);The first log prints js,web. The second log prints web. Object.fromEntries writes tag twice, and the second value replaces the first. Use getAll when repeated values matter.
Quiz: check your understanding
7 QUESTIONSPick your answer before you check it. Then read every explanation.
Question 1 of 7What does
new URL('/docs', 'https://example.com/app/')create?Read the code, then predictconsole.log(new URL("/docs", "https://example.com/app/").href);Choose an answer to see the explanation.
Question 2 of 7What does
url.portprint when the URL uses 443, the usual HTTPS port?Read the code, then predictconst url = new URL("https://example.com:443/a"); console.log(url.port);Choose an answer to see the explanation.
Question 3 of 7How does
URLSearchParamswrite spaces when it turns a query into text?Read the code, then predictconsole.log(new URLSearchParams({ q: "rock and roll" }).toString());Choose an answer to see the explanation.
Question 4 of 7Which call returns every value for a repeated name?
Read the code, then predictconst params = new URLSearchParams("tag=js&tag=web"); console.log(params.getAll("tag").join("|"));Choose an answer to see the explanation.
Question 5 of 7One folder name in a path is
reports/2026. Which encoder makes its slash count as data?Choose an answer to see the explanation.
Question 6 of 7What happens when
decodeURIComponent('%E0%A4%A')runs?Choose an answer to see the explanation.
Question 7 of 7What is
URL.canParse('notes', 'https://example.com/app/')?Read the code, then predictconsole.log(URL.canParse("notes", "https://example.com/app/"));Choose an answer to see the explanation.
Key takeaways
new URL(input, base?)splits an address into parts. It works out a relative address from the base, tidies the parts, and throws aTypeErrorfor an invalid URL.url.searchParamsis linked tourl.search. It is the safest way to build a query string.URLSearchParamswrites a space as+.encodeURIComponentwrites it as%20.encodeURIis for a whole URL.encodeURIComponentis for one piece of data.URL.canParsereturnstrueorfalse.URL.parsereturns a URL ornull, in browsers that have it.
Final definition: A URL object splits an address into parts you can read and change safely. URLSearchParams turns the query string into named values and encodes them for you.
Up next: Streams & progress.