Skip to main content

How to Work with URL Objects and Query Strings in JavaScript

Introduction

Every web application deals with URLs. You parse them to extract query parameters, construct them to build API endpoints, modify them to update search filters, and encode them to safely include user input. Handling URLs with string manipulation is fragile, error-prone, and leads to bugs that are hard to track down. A misplaced &, a missing ?, or an unencoded special character can break an entire request.

JavaScript provides two built-in APIs that make URL handling reliable and clean: the URL object for parsing and constructing complete URLs, and the URLSearchParams object for working with query string parameters. Together, they replace manual string concatenation, regular expressions, and custom parsing functions with a standardized, well-tested interface.

These APIs are available in all modern browsers and in Node.js. They handle edge cases you might not think of: international characters, special symbols, relative URLs, default ports, trailing slashes, and proper encoding. Once you start using them, you will never want to go back to building URLs with template literals and string concatenation.

In this guide, you will learn how to parse URLs into their components, build and modify URLs programmatically, work with query parameters using URLSearchParams, and properly encode special characters for safe URL construction.

new URL(): Parsing URLs

The URL constructor takes a URL string and parses it into an object with properties for each component.

Basic Usage

const url = new URL('https://example.com:8080/path/page?name=alice&age=30#section1');

console.log(url.href); // "https://example.com:8080/path/page?name=alice&age=30#section1"
console.log(url.protocol); // "https:"
console.log(url.host); // "example.com:8080"
console.log(url.hostname); // "example.com"
console.log(url.port); // "8080"
console.log(url.pathname); // "/path/page"
console.log(url.search); // "?name=alice&age=30"
console.log(url.hash); // "#section1"
console.log(url.origin); // "https://example.com:8080"

The constructor parses the string and gives you structured access to every part. No regular expressions, no split() chains, no guessing where the hostname ends and the path begins.

Using a Base URL

The URL constructor accepts an optional second argument: a base URL. This resolves relative URLs against the base, exactly like a browser resolves relative links on a page:

// Relative path resolved against a base
const url1 = new URL('/api/users', 'https://example.com');
console.log(url1.href); // "https://example.com/api/users"

// Relative path with a base that has its own path
const url2 = new URL('users/42', 'https://example.com/api/');
console.log(url2.href); // "https://example.com/api/users/42"

// Going up with ..
const url3 = new URL('../images/logo.png', 'https://example.com/pages/about/');
console.log(url3.href); // "https://example.com/pages/images/logo.png"

// Absolute URL ignores the base
const url4 = new URL('https://other.com/data', 'https://example.com');
console.log(url4.href); // "https://other.com/data"

This is extremely useful for building API endpoints:

const API_BASE = 'https://api.example.com/v2/';

function buildEndpoint(path) {
return new URL(path, API_BASE);
}

console.log(buildEndpoint('users').href);
// "https://api.example.com/v2/users"

console.log(buildEndpoint('users/42/posts').href);
// "https://api.example.com/v2/users/42/posts"
caution

Pay attention to the trailing slash on the base URL. Without it, the last segment of the base path is replaced:

// WITH trailing slash: path is appended
const url1 = new URL('users', 'https://api.example.com/v2/');
console.log(url1.href); // "https://api.example.com/v2/users" ✓

// WITHOUT trailing slash: "v2" is replaced by "users"
const url2 = new URL('users', 'https://api.example.com/v2');
console.log(url2.href); // "https://api.example.com/users" ✗

This follows the same rules as how browsers resolve <a href="..."> tags. If the base has a trailing slash, it is treated as a directory. Without one, the last segment is treated as a file name and gets replaced.

Invalid URLs Throw Errors

If the URL string is not valid, the constructor throws a TypeError:

try {
const url = new URL('not a url');
} catch (error) {
console.log(error.message); // "Invalid URL"
console.log(error instanceof TypeError); // true
}

// These are also invalid (no protocol):
// new URL('example.com') → TypeError
// new URL('//example.com') → TypeError (in most contexts)

// These are valid:
new URL('https://example.com');
new URL('http://localhost:3000');
new URL('ftp://files.example.com');
new URL('data:text/html,<h1>Hello</h1>');

Validating URLs

You can use the URL constructor as a URL validator:

function isValidURL(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}

console.log(isValidURL('https://example.com')); // true
console.log(isValidURL('http://localhost:3000')); // true
console.log(isValidURL('not-a-url')); // false
console.log(isValidURL('')); // false
console.log(isValidURL('ftp://files.example.com')); // true

There is also the static method URL.canParse() (available in modern browsers) which avoids the try/catch:

console.log(URL.canParse('https://example.com'));           // true
console.log(URL.canParse('not-a-url')); // false
console.log(URL.canParse('/path', 'https://example.com')); // true
console.log(URL.canParse('/path')); // false (no base, not absolute)

URL Components: Properties in Detail

A URL has a well-defined structure. Let us dissect every property of the URL object:

https://user:pass@www.example.com:8080/path/to/page?query=value#fragment
|_____| |_______| |_____________| |__||____________||___________||________|
protocol username hostname port pathname search hash
password
|_____________________________|
host
|_______________________________________|
origin
|__________________________________________________________________________|
href

href: The Full URL

The complete URL as a string. Setting this property re-parses the entire URL:

const url = new URL('https://example.com/path');

console.log(url.href); // "https://example.com/path"

// Setting href replaces the entire URL
url.href = 'https://other.com/new-path?x=1';
console.log(url.hostname); // "other.com"
console.log(url.pathname); // "/new-path"
console.log(url.search); // "?x=1"

protocol: The Scheme

The URL scheme, including the trailing colon:

const url = new URL('https://example.com');
console.log(url.protocol); // "https:"

url.protocol = 'http:';
console.log(url.href); // "http://example.com"

// Common protocols
// "http:"
// "https:"
// "ftp:"
// "ws:" (WebSocket)
// "wss:" (WebSocket Secure)
// "data:"
// "blob:"
// "file:"

host vs. hostname

host includes the port (if non-default). hostname is just the domain name:

// With explicit port
const url1 = new URL('https://example.com:8080/path');
console.log(url1.host); // "example.com:8080"
console.log(url1.hostname); // "example.com"

// With default port (443 for HTTPS)
const url2 = new URL('https://example.com/path');
console.log(url2.host); // "example.com" (port omitted because it's default)
console.log(url2.hostname); // "example.com"

// Setting hostname
url1.hostname = 'api.example.com';
console.log(url1.href); // "https://api.example.com:8080/path"

port

The port number as a string. Empty string if the URL uses the default port for its protocol:

const url1 = new URL('https://example.com:8080');
console.log(url1.port); // "8080"

const url2 = new URL('https://example.com');
console.log(url2.port); // "" (default port 443 for HTTPS)

const url3 = new URL('http://example.com');
console.log(url3.port); // "" (default port 80 for HTTP)

// Setting port
url1.port = '3000';
console.log(url1.href); // "https://example.com:3000/"

Default ports by protocol:

  • HTTP: 80
  • HTTPS: 443
  • FTP: 21
  • WS: 80
  • WSS: 443

pathname: The Path

The path portion of the URL, always starting with /:

const url = new URL('https://example.com/api/users/42');
console.log(url.pathname); // "/api/users/42"

const url2 = new URL('https://example.com');
console.log(url2.pathname); // "/"

// Setting pathname
url.pathname = '/api/posts';
console.log(url.href); // "https://example.com/api/posts"

// Pathname is automatically encoded
url.pathname = '/path with spaces/file name.txt';
console.log(url.pathname); // "/path%20with%20spaces/file%20name.txt"

origin: The Origin

A read-only property combining protocol, hostname, and port:

const url = new URL('https://example.com:8080/path?query=1#hash');
console.log(url.origin); // "https://example.com:8080"

const url2 = new URL('https://example.com/path');
console.log(url2.origin); // "https://example.com"

// origin is READ-ONLY
// url.origin = 'https://other.com'; // Has no effect

This is useful for comparing origins (same-origin checks):

function isSameOrigin(url1, url2) {
return new URL(url1).origin === new URL(url2).origin;
}

console.log(isSameOrigin(
'https://example.com/page1',
'https://example.com/page2'
)); // true

console.log(isSameOrigin(
'https://example.com',
'http://example.com'
)); // false (different protocol)

search: The Query String

The query string, including the leading ?. Empty string if there is no query:

const url = new URL('https://example.com/search?q=javascript&page=2');
console.log(url.search); // "?q=javascript&page=2"

const url2 = new URL('https://example.com/path');
console.log(url2.search); // ""

// Setting search replaces the entire query string
url.search = '?q=python&sort=date';
console.log(url.href); // "https://example.com/search?q=python&sort=date"

// Setting to empty removes the query string
url.search = '';
console.log(url.href); // "https://example.com/search"

While you can read and set search as a raw string, the searchParams property (covered in the next section) provides a much better interface for working with individual parameters.

hash: The Fragment

The fragment identifier, including the leading #. Empty string if there is no hash:

const url = new URL('https://example.com/page#section-2');
console.log(url.hash); // "#section-2"

const url2 = new URL('https://example.com/page');
console.log(url2.hash); // ""

url.hash = '#footer';
console.log(url.href); // "https://example.com/page#footer"
note

The hash/fragment is never sent to the server in HTTP requests. It is purely a client-side concept used for in-page navigation and single-page application routing. When you fetch() a URL with a hash, the hash is stripped before the request is sent.

username and password

Credentials embedded in the URL. Rarely used in modern web applications:

const url = new URL('https://admin:secret@example.com/dashboard');
console.log(url.username); // "admin"
console.log(url.password); // "secret"

// Without credentials
const url2 = new URL('https://example.com');
console.log(url2.username); // ""
console.log(url2.password); // ""

searchParams: The Query Parameters Object

The most powerful property. Returns a live URLSearchParams object that provides methods for reading, adding, modifying, and deleting query parameters. Changes to searchParams automatically update the URL's search property and vice versa:

const url = new URL('https://example.com/search?q=javascript&page=2');

console.log(url.searchParams.get('q')); // "javascript"
console.log(url.searchParams.get('page')); // "2"

url.searchParams.set('page', '3');
console.log(url.href); // "https://example.com/search?q=javascript&page=3"

url.searchParams.append('lang', 'en');
console.log(url.href); // "https://example.com/search?q=javascript&page=3&lang=en"

This is covered in full detail in the next section.

Modifying URLs Through Properties

All URL properties (except origin) are writable. Modifying any property automatically updates the full href:

const url = new URL('https://example.com/path');

url.protocol = 'http:';
url.hostname = 'api.example.com';
url.port = '3000';
url.pathname = '/v2/users';
url.search = '?active=true';
url.hash = '#list';

console.log(url.href);
// "http://api.example.com:3000/v2/users?active=true#list"

This makes URL objects excellent for building URLs incrementally:

function buildAPIUrl(endpoint, params = {}) {
const url = new URL(endpoint, 'https://api.example.com');

for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value);
}

return url;
}

const url = buildAPIUrl('/users', { role: 'admin', page: '1', limit: '20' });
console.log(url.href);
// "https://api.example.com/users?role=admin&page=1&limit=20"

toString() and toJSON()

The URL object converts cleanly to a string:

const url = new URL('https://example.com/path?q=test');

// toString() returns href
console.log(url.toString()); // "https://example.com/path?q=test"

// String concatenation calls toString() automatically
console.log('URL is: ' + url); // "URL is: https://example.com/path?q=test"

// toJSON() also returns href (for JSON.stringify)
console.log(url.toJSON()); // "https://example.com/path?q=test"
console.log(JSON.stringify({ link: url })); // '{"link":"https://example.com/path?q=test"}'

// fetch() accepts URL objects directly
const response = await fetch(url);

URLSearchParams: Working with Query Strings

URLSearchParams is a dedicated API for reading and manipulating URL query parameters. It handles encoding, decoding, iteration, and serialization automatically.

Creating URLSearchParams

There are several ways to create a URLSearchParams object:

// From a query string (with or without leading ?)
const params1 = new URLSearchParams('?q=javascript&page=2');
const params2 = new URLSearchParams('q=javascript&page=2');
// Both produce the same result

// From an object
const params3 = new URLSearchParams({
q: 'javascript',
page: '2',
sort: 'relevance'
});

// From an array of pairs
const params4 = new URLSearchParams([
['q', 'javascript'],
['tag', 'web'],
['tag', 'frontend'] // Duplicate keys are allowed
]);

// Empty
const params5 = new URLSearchParams();

// From a URL object's searchParams property
const url = new URL('https://example.com?q=test');
const params6 = url.searchParams; // Live reference, not a copy

Reading Parameters

get(name): Get a Single Value

Returns the first value for the parameter, or null if it does not exist:

const params = new URLSearchParams('q=javascript&page=2&lang=en');

console.log(params.get('q')); // "javascript"
console.log(params.get('page')); // "2"
console.log(params.get('missing')); // null
note

URLSearchParams always returns strings. Even numeric values are returned as strings. You must convert them yourself:

const params = new URLSearchParams('page=3&limit=20&active=true');

console.log(typeof params.get('page')); // "string"
console.log(params.get('page')); // "3" (not the number 3)

const page = Number(params.get('page')); // 3
const limit = parseInt(params.get('limit')); // 20
const active = params.get('active') === 'true'; // true

getAll(name): Get All Values

Returns an array of all values for a parameter. Essential for multi-value parameters:

const params = new URLSearchParams('color=red&color=blue&color=green');

console.log(params.get('color')); // "red" (only the first)
console.log(params.getAll('color')); // ["red", "blue", "green"]

const params2 = new URLSearchParams('single=value');
console.log(params2.getAll('single')); // ["value"]
console.log(params2.getAll('missing')); // []

has(name) and has(name, value): Check Existence

const params = new URLSearchParams('q=javascript&page=2');

console.log(params.has('q')); // true
console.log(params.has('missing')); // false

// Check for a specific name AND value pair
console.log(params.has('q', 'javascript')); // true
console.log(params.has('q', 'python')); // false

Modifying Parameters

set(name, value): Set (Replace) a Parameter

Replaces all values for the given name with a single new value. If the parameter does not exist, it is added:

const params = new URLSearchParams('q=javascript&page=2');

params.set('page', '5');
console.log(params.toString()); // "q=javascript&page=5"

// If multiple values exist, set() replaces ALL of them with one
const params2 = new URLSearchParams('tag=web&tag=js&tag=css');
params2.set('tag', 'python');
console.log(params2.toString()); // "tag=python"

// Adding a new parameter
params.set('sort', 'date');
console.log(params.toString()); // "q=javascript&page=5&sort=date"

append(name, value): Add a Parameter

Adds a new name-value pair without removing existing ones with the same name:

const params = new URLSearchParams('q=javascript');

params.append('tag', 'web');
params.append('tag', 'frontend');
params.append('tag', 'beginner');

console.log(params.toString());
// "q=javascript&tag=web&tag=frontend&tag=beginner"

console.log(params.getAll('tag'));
// ["web", "frontend", "beginner"]

set() vs. append():

const params = new URLSearchParams();

// append: adds, allows duplicates
params.append('color', 'red');
params.append('color', 'blue');
console.log(params.toString()); // "color=red&color=blue"

// set: replaces ALL existing values
params.set('color', 'green');
console.log(params.toString()); // "color=green"

delete(name) and delete(name, value): Remove Parameters

Removes all entries with the given name:

const params = new URLSearchParams('q=javascript&page=2&sort=date');

params.delete('sort');
console.log(params.toString()); // "q=javascript&page=2"

// Delete only a specific name-value pair
const params2 = new URLSearchParams('tag=web&tag=js&tag=css');
params2.delete('tag', 'js');
console.log(params2.toString()); // "tag=web&tag=css"

Iterating Over Parameters

URLSearchParams is iterable and provides several iteration methods:

const params = new URLSearchParams('q=javascript&page=2&sort=date');

// for...of (default: entries)
for (const [key, value] of params) {
console.log(`${key} = ${value}`);
}
// q = javascript
// page = 2
// sort = date

// .entries()
for (const [key, value] of params.entries()) {
console.log(`${key}: ${value}`);
}

// .keys()
for (const key of params.keys()) {
console.log(key); // "q", "page", "sort"
}

// .values()
for (const value of params.values()) {
console.log(value); // "javascript", "2", "date"
}

// .forEach()
params.forEach((value, key) => {
console.log(`${key}${value}`);
});

Converting to String

toString() serializes all parameters into a query string without the leading ?:

const params = new URLSearchParams({
q: 'javascript tutorial',
page: '1',
lang: 'en'
});

console.log(params.toString());
// "q=javascript+tutorial&page=1&lang=en"

// Note: spaces are encoded as + (not %20) in URLSearchParams.toString()
// This is the application/x-www-form-urlencoded format

Sorting Parameters

sort() sorts all parameters alphabetically by name. This is useful for creating canonical URLs (for caching, comparison, or signatures):

const params = new URLSearchParams('z=last&a=first&m=middle');

params.sort();
console.log(params.toString()); // "a=first&m=middle&z=last"

size Property

Returns the total number of parameter entries:

const params = new URLSearchParams('a=1&b=2&c=3&b=4');
console.log(params.size); // 4 (b appears twice)

URLSearchParams and URL Objects Work Together

The searchParams property of a URL object is a live URLSearchParams. Changes to either one update the other:

const url = new URL('https://example.com/search?q=test');

// Modify through searchParams
url.searchParams.set('page', '2');
url.searchParams.append('lang', 'en');

// URL is automatically updated
console.log(url.href);
// "https://example.com/search?q=test&page=2&lang=en"

// Modify through search property
url.search = '?newparam=value';

// searchParams is automatically updated
console.log(url.searchParams.get('newparam')); // "value"
console.log(url.searchParams.get('q')); // null (old params are gone)

Practical Examples

Building a Search URL

function buildSearchURL(baseURL, filters) {
const url = new URL(baseURL);

for (const [key, value] of Object.entries(filters)) {
if (value === null || value === undefined || value === '') continue;

if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else {
url.searchParams.set(key, String(value));
}
}

return url;
}

const searchURL = buildSearchURL('https://api.example.com/products', {
q: 'laptop',
minPrice: 500,
maxPrice: 1500,
brand: ['Apple', 'Dell', 'Lenovo'],
inStock: true,
color: '' // empty values are skipped
});

console.log(searchURL.href);
// "https://api.example.com/products?q=laptop&minPrice=500&maxPrice=1500&brand=Apple&brand=Dell&brand=Lenovo&inStock=true"

Parsing Current Page URL

// Get the current page's query parameters
const currentParams = new URLSearchParams(window.location.search);

const page = parseInt(currentParams.get('page')) || 1;
const query = currentParams.get('q') || '';
const sort = currentParams.get('sort') || 'relevance';

console.log(`Page ${page}, searching for "${query}", sorted by ${sort}`);

Updating URL Without Page Reload

function updateQueryParam(key, value) {
const url = new URL(window.location.href);

if (value === null || value === undefined) {
url.searchParams.delete(key);
} else {
url.searchParams.set(key, value);
}

// Update browser URL without reloading
window.history.pushState({}, '', url);
}

// User changes page
updateQueryParam('page', '3');
// URL: https://mysite.com/products?page=3

// User clears a filter
updateQueryParam('category', null);
// URL: https://mysite.com/products?page=3 (category removed)

Converting URLSearchParams to a Plain Object

function paramsToObject(params) {
const obj = {};

for (const key of new Set(params.keys())) {
const values = params.getAll(key);
obj[key] = values.length === 1 ? values[0] : values;
}

return obj;
}

const params = new URLSearchParams('name=Alice&hobby=reading&hobby=coding&age=30');
console.log(paramsToObject(params));
// { name: "Alice", hobby: ["reading", "coding"], age: "30" }

Using URLSearchParams with fetch()

URLSearchParams integrates cleanly with fetch() in two ways:

// 1. As a query string in a URL
const params = new URLSearchParams({ q: 'javascript', limit: '10' });
const response = await fetch(`https://api.example.com/search?${params}`);

// 2. As a POST body (automatically sets Content-Type)
const loginParams = new URLSearchParams({
username: 'alice',
password: 'secret123'
});

const response = await fetch('https://example.com/login', {
method: 'POST',
body: loginParams
// Content-Type is automatically set to application/x-www-form-urlencoded
});

Encoding: encodeURIComponent and encodeURI

URLs can only contain a specific set of ASCII characters. Any character outside this set (spaces, international characters, special symbols) must be percent-encoded: replaced with a % followed by the character's hexadecimal code. JavaScript provides two functions for this purpose, and understanding the difference between them is critical.

encodeURIComponent(): Encode a Single Value

encodeURIComponent() encodes a component (a single value) of a URL. It encodes almost everything except letters, digits, and the characters - _ . ~ ! * ' ( ).

Crucially, it encodes characters that have special meaning in URLs: / ? # & = + : @ $ ,

console.log(encodeURIComponent('hello world'));
// "hello%20world"

console.log(encodeURIComponent('price=10&currency=€'));
// "price%3D10%26currency%3D%E2%82%AC"

console.log(encodeURIComponent('user@example.com'));
// "user%40example.com"

console.log(encodeURIComponent('path/to/file'));
// "path%2Fto%2Ffile"

console.log(encodeURIComponent('café'));
// "caf%C3%A9"

console.log(encodeURIComponent('日本語'));
// "%E6%97%A5%E6%9C%AC%E8%AA%9E"

Use encodeURIComponent for:

  • Query parameter values
  • Path segments that might contain special characters
  • Any single piece of data being inserted into a URL
const searchQuery = 'JavaScript & TypeScript: "best practices"';
const category = 'tutorials/web dev';

// CORRECT: Encode each value separately
const url = `https://api.example.com/search?q=${encodeURIComponent(searchQuery)}&cat=${encodeURIComponent(category)}`;

console.log(url);
// "https://api.example.com/search?q=JavaScript%20%26%20TypeScript%3A%20%22best%20practices%22&cat=tutorials%2Fweb%20dev"

encodeURI(): Encode a Full URL

encodeURI() encodes a complete URL string. It is less aggressive than encodeURIComponent because it preserves characters that are part of the URL structure: : / ? # [ ] @ ! $ & ' ( ) * + , ; =

console.log(encodeURI('https://example.com/path to page?q=hello world'));
// "https://example.com/path%20to%20page?q=hello%20world"
// Notice: :, /, ?, = are NOT encoded (they're part of URL structure)

console.log(encodeURI('https://example.com/café/résumé'));
// "https://example.com/caf%C3%A9/r%C3%A9sum%C3%A9"
// Non-ASCII characters ARE encoded, but / is preserved

Use encodeURI for:

  • Encoding an entire URL string that contains non-ASCII characters
  • Almost never in practice (use URL object or encodeURIComponent instead)

The Critical Difference: Why It Matters

Using the wrong function can break your URLs or create security issues:

const userInput = 'tom&jerry=friends';

// WRONG: encodeURI does NOT encode & and =
const badURL = `https://api.example.com/search?q=${encodeURI(userInput)}`;
console.log(badURL);
// "https://api.example.com/search?q=tom&jerry=friends"
// This creates TWO parameters: q=tom and jerry=friends (NOT what we want!)

// CORRECT: encodeURIComponent encodes & and =
const goodURL = `https://api.example.com/search?q=${encodeURIComponent(userInput)}`;
console.log(goodURL);
// "https://api.example.com/search?q=tom%26jerry%3Dfriends"
// This creates ONE parameter: q=tom&jerry=friends (correct!)

decodeURIComponent() and decodeURI()

The corresponding decode functions reverse the encoding:

console.log(decodeURIComponent('hello%20world'));
// "hello world"

console.log(decodeURIComponent('caf%C3%A9'));
// "café"

console.log(decodeURIComponent('%E6%97%A5%E6%9C%AC%E8%AA%9E'));
// "日本語"

console.log(decodeURI('https://example.com/caf%C3%A9'));
// "https://example.com/café"
caution

Calling decodeURIComponent on a string that contains a bare % followed by invalid hex digits throws a URIError:

try {
decodeURIComponent('%'); // URIError: URI malformed
} catch (error) {
console.log(error.message); // "URI malformed"
}

// Always wrap in try/catch when decoding user-provided strings
function safeDecode(str) {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}

URL Object Handles Encoding Automatically

One of the biggest advantages of using URL and URLSearchParams is that they handle encoding and decoding automatically. You rarely need to call encodeURIComponent manually:

// URLSearchParams encodes values automatically
const params = new URLSearchParams();
params.set('q', 'JavaScript & TypeScript');
params.set('author', 'José García');

console.log(params.toString());
// "q=JavaScript+%26+TypeScript&author=Jos%C3%A9+Garc%C3%ADa"
// Everything is properly encoded!

// And decodes when you read
console.log(params.get('q')); // "JavaScript & TypeScript"
console.log(params.get('author')); // "José García"

// URL object also encodes pathname automatically
const url = new URL('https://example.com');
url.pathname = '/path with spaces/café';
console.log(url.pathname); // "/path%20with%20spaces/caf%C3%A9"
console.log(url.href); // "https://example.com/path%20with%20spaces/caf%C3%A9"

When You Still Need Manual Encoding

Despite the URL object handling most cases, there are situations where you need encodeURIComponent:

// Building a URL string without the URL object
const apiKey = 'key=abc&secret=123';
const endpoint = `https://api.example.com/data?apiKey=${encodeURIComponent(apiKey)}`;

// Encoding values for non-URL contexts (like cookies)
document.cookie = `username=${encodeURIComponent('José García')}; path=/`;

// Building mailto: links
const subject = encodeURIComponent('Question about "JavaScript"');
const body = encodeURIComponent('Hello,\nI have a question...');
const mailto = `mailto:support@example.com?subject=${subject}&body=${body}`;

Encoding Summary

FunctionEncodesPreservesUse For
encodeURIComponent()Almost everythingA-Z a-z 0-9 - _ . ~ ! * ' ( )Single values (query params, path segments)
encodeURI()Non-ASCII characters, spacesURL structural characters : / ? # & = + @Complete URL strings (rarely needed)
URLSearchParamsHandles automaticallyN/AQuery parameters (preferred)
URL objectHandles automaticallyN/AFull URLs (preferred)

Common Patterns and Best Practices

Pattern: API Client with URL Builder

class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
}

buildURL(path, params = {}) {
const url = new URL(path, this.baseURL);

for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else {
url.searchParams.set(key, String(value));
}
}
}

return url;
}

async get(path, params = {}) {
const url = this.buildURL(path, params);
const response = await fetch(url);

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

return response.json();
}
}

const api = new ApiClient('https://api.example.com/v2/');

// Clean API calls with automatic URL construction
const users = await api.get('users', {
role: 'admin',
active: true,
fields: ['name', 'email', 'role']
});
// Fetches: https://api.example.com/v2/users?role=admin&active=true&fields=name&fields=email&fields=role

Pattern: Extracting and Comparing URL Parts

function getURLInfo(urlString) {
const url = new URL(urlString);

return {
domain: url.hostname,
path: url.pathname,
isSecure: url.protocol === 'https:',
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
params: Object.fromEntries(url.searchParams),
hasFragment: url.hash !== ''
};
}

const info = getURLInfo('https://shop.example.com:8443/products?category=electronics&sort=price#filters');
console.log(info);
// {
// domain: "shop.example.com",
// path: "/products",
// isSecure: true,
// port: "8443",
// params: { category: "electronics", sort: "price" },
// hasFragment: true
// }

Pattern: URL Normalization

function normalizeURL(urlString) {
const url = new URL(urlString);

// Lowercase hostname
// (URL constructor does this automatically)

// Remove default ports
// (URL constructor does this automatically)

// Remove trailing slash from path (optional, depends on your convention)
if (url.pathname.length > 1 && url.pathname.endsWith('/')) {
url.pathname = url.pathname.slice(0, -1);
}

// Sort query parameters for consistent comparison
url.searchParams.sort();

// Remove the fragment (not sent to server)
url.hash = '';

return url.href;
}

// These all normalize to the same URL
console.log(normalizeURL('HTTPS://EXAMPLE.COM:443/path/?b=2&a=1#hash'));
console.log(normalizeURL('https://example.com/path?a=1&b=2'));
// Both: "https://example.com/path?a=1&b=2"

Common Mistake: String Concatenation Without Encoding

const userInput = 'search term with spaces & special=chars';

// WRONG: Unencoded special characters break the URL
const badURL = 'https://api.example.com/search?q=' + userInput;
console.log(badURL);
// "https://api.example.com/search?q=search term with spaces & special=chars"
// Spaces, &, and = break the URL structure!

// CORRECT: Use URL and URLSearchParams
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', userInput);
console.log(url.href);
// "https://api.example.com/search?q=search+term+with+spaces+%26+special%3Dchars"

Common Mistake: Double Encoding

// WRONG: Encoding a value and then putting it in URLSearchParams
const value = 'hello world';
const encoded = encodeURIComponent(value); // "hello%20world"
const params = new URLSearchParams();
params.set('q', encoded);
console.log(params.toString());
// "q=hello%2520world" (%25 is the encoding of %, so %20 became %2520!)

// CORRECT: Let URLSearchParams handle encoding
const params2 = new URLSearchParams();
params2.set('q', value); // Pass raw value
console.log(params2.toString());
// "q=hello+world" (correctly encoded once)

Note: + vs. %20 for Spaces

URLSearchParams encodes spaces as + (the application/x-www-form-urlencoded standard), while encodeURIComponent encodes spaces as %20. Both are valid, but servers may expect one over the other:

// URLSearchParams: spaces → +
const params = new URLSearchParams({ q: 'hello world' });
console.log(params.toString()); // "q=hello+world"

// encodeURIComponent: spaces → %20
console.log(encodeURIComponent('hello world')); // "hello%20world"

// Both decode to the same value
console.log(decodeURIComponent('hello%20world')); // "hello world"
console.log(decodeURIComponent('hello+world')); // "hello+world" (+ is not decoded!)

// URLSearchParams handles both when parsing:
const p1 = new URLSearchParams('q=hello+world');
const p2 = new URLSearchParams('q=hello%20world');
console.log(p1.get('q')); // "hello world"
console.log(p2.get('q')); // "hello world"

Summary

The URL object parses any URL string into its components: protocol, hostname, port, pathname, search, hash, origin, and more. All properties (except origin) are writable, and modifying any property automatically updates the full URL. The constructor accepts an optional base URL for resolving relative paths, and it throws a TypeError for invalid URLs, making it useful for validation.

URLSearchParams provides a clean API for working with query string parameters: get() and getAll() for reading, set() and append() for writing, delete() for removing, has() for checking existence, and full iteration support with for...of, .entries(), .keys(), .values(), and .forEach(). It handles encoding and decoding automatically, so you never need to worry about special characters in parameter values.

For encoding, use encodeURIComponent() when manually inserting values into URL strings. It encodes everything that could interfere with URL structure, including &, =, /, ?, and #. Use encodeURI() only for encoding a complete URL string while preserving its structure. In practice, prefer URL and URLSearchParams objects, which handle encoding automatically and eliminate the risk of double-encoding or missed encoding.

The golden rule: stop building URLs with string concatenation and template literals. Use the URL and URLSearchParams APIs for correct, safe, and readable URL handling in every situation.