Skip to content
JavaScript Basics: Build and Edit URLs With URLSearchParams

JavaScript Basics: Build and Edit URLs With URLSearchParams

October 14, 2026

A search term such as tea & cake needs special handling inside a URL. Joining strings by hand can accidentally turn part of the term into another query parameter. JavaScript’s URL and URLSearchParams objects let us work with the pieces separately and handle query encoding for us.

Prepare a scratch directory

You need Linux, Bash, the mktemp, rm, and rmdir utilities, a text editor, and Node.js on your PATH. These examples were tested with Node.js v24.19.0 on Linux. They use built-in features only: no npm packages, package manifest, custom environment variables, or network requests. Creating a URL object does not visit that address.

Run in Bash:

node --version
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

The tested runtime prints:

v24.19.0

Keep this terminal open. Use your editor to save each source block as a UTF-8 file with the stated filename in this temporary directory. The .mjs extension selects ES modules, so the scripts can import Node’s built-in assertion module. Assertions stay silent when their checks pass.

Build a search link and change its filters

Save as build-search-link.mjs:

import assert from "node:assert/strict";

const url = new URL("https://example.com/search");
url.searchParams.set("q", "tea & cake");
url.searchParams.set("page", "2");
url.searchParams.append("tag", "javascript");
url.searchParams.append("tag", "node");

assert.equal(url.searchParams.get("q"), "tea & cake");
assert.equal(url.searchParams.get("tag"), "javascript");
assert.deepEqual(url.searchParams.getAll("tag"), ["javascript", "node"]);
console.log(url.href);
console.log(`Tags: ${JSON.stringify(url.searchParams.getAll("tag"))}`);

url.searchParams.set("tag", "beginner");
url.searchParams.delete("page");
url.hash = "results";

assert.deepEqual(url.searchParams.getAll("tag"), ["beginner"]);
assert.equal(url.searchParams.has("page"), false);
assert.equal(url.pathname, "/search");
console.log(url.href);

Run:

node build-search-link.mjs

Expected output:

https://example.com/search?q=tea+%26+cake&page=2&tag=javascript&tag=node
Tags: ["javascript","node"]
https://example.com/search?q=tea+%26+cake&tag=beginner#results

set keeps one value for a name, replacing any duplicates. append adds another pair, which is useful for repeated filters. get reads the first value; getAll returns every value. Changing url.searchParams also updates the parent URL. The hash property holds the fragment after #.

Pass ordinary, unencoded values to set and append. Query serialization turns the spaces into + and the ampersand into %26, keeping it inside the search term. Pre-encoding the value would encode the percent signs again. See MDN’s URLSearchParams reference.

Tell missing values, empty values, and plus signs apart

Save as inspect-query-values.mjs:

import assert from "node:assert/strict";

const params = new URLSearchParams("q=hello+world&empty=");
assert.equal(params.get("q"), "hello world");
assert.equal(params.get("empty"), "");
assert.equal(params.get("missing"), null);
console.log(`Empty: ${JSON.stringify(params.get("empty"))}`);
console.log(`Missing: ${JSON.stringify(params.get("missing"))}`);

params.set("language", "C++");
assert.equal(params.get("language"), "C++");
assert.equal(new URLSearchParams(params.toString()).get("language"), "C++");
console.log(params.toString());

params.set("city", "東京");
assert.equal(new URLSearchParams(params.toString()).get("city"), "東京");
assert.equal(new URLSearchParams("").get("q"), null);
console.log(`Unicode round trip: ${params.get("city")}`);

Run:

node inspect-query-values.mjs

Expected output:

Empty: ""
Missing: null
q=hello+world&empty=&language=C%2B%2B
Unicode round trip: 東京

Parsing a query string treats + as a space. Setting the literal value C++ lets the serializer encode its plus signs correctly. The values returned by get are strings or null, so a parameter such as page=2 still needs conversion and validation before you use it as a number. toString() returns the query without a leading ?.

Give URLSearchParams a query string, not a complete URL. To inspect an existing full address, construct a URL first and read its searchParams. Node’s URL documentation covers these constructors and methods.

Resolve relative paths and handle invalid input

Save as resolve-url-paths.mjs:

import assert from "node:assert/strict";

const base = "https://example.com/docs/";
const child = new URL("intro", base);
const root = new URL("/intro", base);
const withoutSlash = new URL("intro", "https://example.com/docs");
assert.equal(child.href, "https://example.com/docs/intro");
assert.equal(root.href, "https://example.com/intro");
assert.equal(withoutSlash.href, root.href);
console.log(`Child: ${child.href}`);
console.log(`Root: ${root.href}`);
console.log(`No trailing slash: ${withoutSlash.href}`);

for (const input of ["/intro", "https://"]) {
  try {
    new URL(input);
    assert.fail("Expected an invalid URL");
  } catch (error) {
    assert.ok(error instanceof TypeError);
    console.log(`Rejected ${JSON.stringify(input)}: ${error.name}`);
  }
}

const otherHost = new URL("//other.example/intro", base);
assert.equal(otherHost.origin, "https://other.example");
console.log(`Changed origin: ${otherHost.origin}`);

Run:

node resolve-url-paths.mjs

Expected output:

Child: https://example.com/docs/intro
Root: https://example.com/intro
No trailing slash: https://example.com/intro
Rejected "/intro": TypeError
Rejected "https://": TypeError
Changed origin: https://other.example

A relative input needs a base URL. With a trailing slash, docs/ is a directory; without it, docs is the last path segment to replace. A leading / starts from the site’s root. MDN explains relative URL resolution.

The two invalid inputs deliberately exercise the constructor’s TypeError path. Parsing successfully only establishes that an input forms a URL. It does not establish that the destination is appropriate for your application: an input beginning with // can replace the host even when you supply a base. Check the parsed protocol and origin against your requirements before using untrusted destinations. See MDN’s URL constructor documentation.

Clean up

After running all three scripts, remove only the tutorial files:

rm -- build-search-link.mjs inspect-query-values.mjs resolve-url-paths.mjs
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

Use URL for the address as a whole and URLSearchParams for its query values. Keeping those jobs separate makes spaces, punctuation, repeated filters, and relative paths much easier to reason about.

Acknowledgements

This post was created with assistance from AI.

Last updated on