Command Palette

Search for a command to run...

Hectal
PHASE 11Intermediate ~21 min· topic 4 of 8

Topic 11.4

URL, URLSearchParams and location

In one line

The URL class splits any web address into reliable parts and resolves relative links, URLSearchParams reads and builds query strings with correct encoding, and location is the current page's URL, which you can read and change to navigate.

Think of it like this

A postal address on an envelope. It has a fixed order: country, city, street, house number, and maybe "attention: Asha" at the bottom. If you cut the address up by hand with scissors, you'll eventually cut in the wrong place (what if the street name contains a comma?). The URL class is a post-office machine that reads any address and hands you each part, cut correctly every time.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

URL
A web address, like https://hectal.in/javascript?tab=1#top.
Hostname and host
The site's name (hectal.in); the host is the hostname plus the port if one is written.
Pathname
The part after the host that says which page or file: /javascript/roadmap.
Query string
The part after ?: name=value pairs joined by &. Also called the search.
Hash (fragment)
The part after #. It points inside the page and is never sent to the server.
Relative URL
A partial address like ../about that only makes sense next to a base URL.
Percent-encoding
Writing special characters as % plus a code, like a space as %20, so they travel safely inside a URL.
location
The browser object for the current page's URL. Changing it can load a new page.

Step by step

01Name the parts

Every part has a property with the same name on URL objects and on location.

Name the partsdiagram
Rendering diagram…

02Parse instead of splitting strings

url.split("?")[1].split("&") works until a value contains &, = or #, or the URL has no query at all. The parser handles every case.

parse.jswhole filejavascript
const url = new URL("https://shop.example.com/search?q=tea%20%26%20coffee&page=2");
console.log(url.searchParams.get("q"));            // "tea & coffee"
console.log(Number(url.searchParams.get("page"))); // 2

03Build a URL safely

Start from a base, then set parameters. Encoding is automatic, and user input can't sneak in an extra &admin=1.

build.jswhole filejavascript
const url = new URL("/api/search", "https://api.example.com");
url.searchParams.set("q", userInput);
url.searchParams.set("limit", "20");
const res = await fetch(url); // fetch accepts URL objects

04Read the current page's query

On a real page with the address https://site.com/products?sort=price&tag=new, read the filters on load and fill in the form, so a shared link reopens the same view.

filters.jswhole filejavascript
const params = new URLSearchParams(location.search);
const sort = params.get("sort") ?? "popular";
const tags = params.getAll("tag");

05Navigate with location

assign and setting href add a history entry; replace doesn't, which suits redirects (after login you don't want Back to return to the login form).

navigate.jswhole filejavascript
location.assign("/dashboard");   // new page, Back returns here
location.replace("/dashboard");  // new page, Back skips this one
location.hash = "#comments";     // same page: scroll + hashchange, no reload

Try it yourself

  1. 1

    Parse your own address

    In the first example, replace the URL with https://hectal.in/javascript/topic/phase-11/url-apis?ref=home#tryit. Predict pathname, search and hash before running.

  2. 2

    Repeat a key

    In the second example, tick both sort options by turning the <select> into <select name="sort" multiple> and selecting both. Predict the link: one sort or two?

  3. 3

    Find the default port

    Predict, then log new URL("https://a.com:443/x").port and new URL("http://a.com:443/x").port. Why is one of them empty?

Code & diagrams

Take a URL apart New tab

`+` in a query means a space, so `get("q")` gives `red shoes`. A path alone isn't a full URL: give a base as the second argument.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

protocol https:
hostname shop.example.com
port 8080
host shop.example.com:8080
origin https://shop.example.com:8080
pathname /search
search ?q=red+shoes&page=2
hash #reviews
q is red shoes
https://shop.example.com:8080/search?q=red+shoes&page=3
https://a.com/shop/img/a.png
https://a.com/list?page=2
false
TypeError: Failed to construct 'URL': Invalid URL
URLSearchParams: build, read and encode New tab

`Object.fromEntries` keeps only the **last** `tag`, so use `getAll` for repeated keys. In the unencoded URL, the server would read `q` as `tea ` and see a second, empty parameter called ` coffee`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

q=tea+%26+coffee&sort=price&tag=hot&tag=new
tea & coffee [ 'hot', 'new' ] null 4
{ q: 'tea & coffee', sort: 'price', tag: 'new' }
q=tea+%26+coffee&sort=price&page=2
/search?q=tea & coffee
/search?q=tea%20%26%20coffee
https://shop.example.com/search?q=tea+%26+coffee&sort=rating
location in the preview, and hash changes New tab

Hectal runs previews in a sandboxed `srcdoc` frame, so `location.href` is `about:srcdoc`. Changing the hash updates `location` at once, but `hashchange` fires a moment later, as a task. Click the links to fire it again.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

href: about:srcdoc
protocol: about:
right after setting: #details
hashchange -> #details

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Build a URL with string gluing

In a search box, build the request as fetch("/api/search?q=" + input.value) and type R&D #1.

terminal
$ Network tab
── what you'll see ──
Request URL: /api/search?q=R&D%20 (the server sees q="R", a parameter called "D ", and "#1" is never sent)

Break #2

Parse a relative path without a base

Write new URL("/products?id=3").

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Failed to construct 'URL': Invalid URL

Myth vs fact

Myth

params.get("page") returns a number when the URL says page=2.

Fact

Everything in a query string is text. You get "2"; convert with Number() and handle null when it's missing.

Myth

The #hash is sent to the server.

Fact

Browsers strip the fragment before sending the request. That's why old single-page apps used #/routes: the server always sees the same URL.

Myth

encodeURI is the right way to encode a query value.

Fact

encodeURI leaves &, =, ? and # alone because it expects a whole URL. For a single value, use encodeURIComponent or URLSearchParams.

Myth

Changing location.search from JavaScript only updates the address bar.

Fact

Setting location.search (or href, pathname) loads a new page. To change the address without reloading, use the History API (next topic).

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The URL class follows the WHATWG URL Standard, the same parser the browser uses for links and fetch, so it agrees with the browser on odd inputs (backslashes, tabs, extra slashes). Hand-written regexes don't, which has led to real security bugs such as open-redirect checks that a parser disagrees with.

  • ▸

    When checking where a redirect goes, compare new URL(target, location.origin).origin to your own origin. Checking that a string "starts with /" isn't enough: //evil.example is a protocol-relative URL to another site.

  • ▸

    URLSearchParams uses the application/x-www-form-urlencoded rules: spaces become + and *-._ stay as they are. encodeURIComponent leaves !'()* unescaped. Servers decode both correctly, but signing a URL (for example, cloud storage signatures) requires the exact encoding the signer expects.

  • ▸

    Text fragments (#:~:text=some%20words) let links highlight a phrase in a page; they're handled by the browser and aren't visible in location.hash in Chromium.

Remember this

  1. 1

    A URL (Uniform Resource Locator) is a web address. Take https://shop.example.com:8080/search?q=red+shoes&page=2#reviews. The protocol (or scheme) is https:. The hostname is shop.example.com, and with the port (8080) it makes the host shop.example.com:8080. The origin is protocol + host: https://shop.example.com:8080 (Topic 11.1). The pathname is /search. The search (or query string) is ?q=red+shoes&page=2: name=value pairs joined by &. The hash (or fragment) is #reviews: a place inside the page, which the browser never sends to the server.

  2. 2

    new URL(text) parses an address into an object with all those parts as properties: protocol, host, hostname, port, pathname, search, hash, origin and the full href. They are settable: change url.pathname or url.hash and href updates. If the text isn't a valid absolute URL, the constructor throws a TypeError (Failed to construct 'URL': Invalid URL). URL.canParse(text) checks first without throwing, and URL.parse(text) returns null instead of throwing (both newer additions, in all current browsers).

  3. 3

    new URL(relative, base) resolves a relative link the same way the browser does for <a href>: new URL("../img/a.png", "https://a.com/shop/items/1") gives https://a.com/shop/img/a.png. "?page=2" keeps the path and replaces the query; "#top" keeps everything but the hash; "//cdn.com/a.js" keeps only the protocol. The parser also normalises: it lowercases the hostname, removes the default port (:443 for https), resolves ./ and ../, and turns international domain names into their ASCII form (münchen.de becomes xn--mnchen-3ya.de).

  4. 4

    URLSearchParams handles the query string. url.searchParams is a live one attached to a URL (changes update the URL), or you can make one with new URLSearchParams("a=1&b=2"), from an object, or from a form's FormData. Methods: get(name) (the first value, or null), getAll(name) (every value, for repeated keys like tag=a&tag=b), has, set (replace), append (add another), delete, sort, size, and iteration with for...of. toString() gives the query without the ?. Every value comes back as a string: Number(params.get("page")).

  5. 5

    Encoding turns characters that have a meaning in URLs (&, =, ?, #, /, spaces, non-English letters) into % codes so they arrive as plain data. URLSearchParams does it for you, writing a space as + (the form encoding). encodeURIComponent(value) encodes one piece, writing a space as %20. encodeURI encodes a whole URL but leaves &, ? and / alone, so it's wrong for a single value. Building URLs by gluing strings ("?q=" + term) breaks the moment the term contains & or #, and can let user input change other parameters.

  6. 6

    location (also window.location) is the URL of the current page, with the same properties as a URL object. Reading it is how you get the current query string: new URLSearchParams(location.search). Writing it navigates: location.href = "/login" or location.assign("/login") loads a new page and adds a history entry, location.replace("/login") loads it without a history entry (the Back button skips it), and location.reload() reloads. Changing only location.hash scrolls to the target and fires hashchange without reloading. In Hectal's sandboxed preview, location.href is about:srcdoc, so examples parse fixed URLs instead.

Explain it without notes

01

Name the parts of a URL and what each is used for.

02

Why use URL/URLSearchParams instead of string concatenation and split?

03

What is the difference between location.assign, location.replace and setting location.hash?

04

How does new URL(relative, base) resolve ../a, ?x=1, #y and //host/z?

Practice

01

Write getPage(urlText) that returns the page query parameter as a number, or 1 when it's missing or not a positive whole number.

02

Write withParams(base, obj) that returns a URL string with every key of obj added as a query parameter, skipping null and undefined values.

Trade-offs

  • ↔

    Keeping UI state (filters, tabs, page number) in the query string makes views shareable and bookmarkable and survives reloads, but every change needs URL updates and very large state makes ugly, long URLs; keep only what a shared link should restore.

  • ↔

    location.replace keeps history tidy for redirects but surprises users if used for normal navigation, because Back no longer goes where they expect.

Done when you can

  • I can name every part of a URL and read it from a URL object.

  • I can resolve relative URLs with a base.

  • I build and read query strings with URLSearchParams, including repeated keys.

  • I know when to use encodeURIComponent and why string gluing is dangerous.

  • I can navigate with location.assign, replace and hash, and know which add history entries.