Introduction
Working with URLs in JavaScript used to be a messy affair involving string manipulation, regular expressions, and brittle parsing logic. The modern URL() and URLSearchParams APIs changed everything, providing robust, standardized interfaces for URL manipulation that solve countless headaches developers face daily.
In this comprehensive guide, we'll explore these APIs in depth, understanding not just how they work, but why they exist and the specific problems they solve.
The URL() Constructor: Understanding Web Addresses
What is URL()?
The URL() constructor creates URL objects that represent and parse Uniform Resource Locators. It takes a URL string (and optionally a base URL) and returns an object with properties representing each component of the URL.
Basic Syntax
new URL(url)
new URL(url, base)
Parameters:
-
url(required): An absolute or relative URL string -
base(optional): A base URL string to resolve relative URLs against
Anatomy of a URL Object
When you create a URL object, it automatically parses the URL into its constituent parts:
const url = new URL('https://john:secret@example.com:8080/path/to/page?name=John&age=30#section');
console.log(url.href); // "https://john:secret@example.com:8080/path/to/page?name=John&age=30#section"
console.log(url.protocol); // "https:"
console.log(url.username); // "john"
console.log(url.password); // "secret"
console.log(url.host); // "example.com:8080"
console.log(url.hostname); // "example.com"
console.log(url.port); // "8080"
console.log(url.pathname); // "/path/to/page"
console.log(url.search); // "?name=John&age=30"
console.log(url.hash); // "#section"
console.log(url.origin); // "https://example.com:8080"
Key Properties Explained
1. href - The complete URL string. This is both readable and writable:
const url = new URL('https://example.com/page');
url.href = 'https://newsite.com/newpage';
console.log(url.hostname); // "newsite.com"
2. protocol - The scheme of the URL (always includes the colon):
const url = new URL('https://example.com');
url.protocol = 'http:';
console.log(url.href); // "http://example.com"
3. username and password - Credentials in the URL (rarely used in modern web development due to security concerns):
const url = new URL('https://example.com');
url.username = 'admin';
url.password = 'pass123';
console.log(url.href); // "https://admin:pass123@example.com"
4. host vs hostname - A critical distinction:
-
hostincludes the port number -
hostnameis just the domain name
const url = new URL('https://example.com:3000');
console.log(url.host); // "example.com:3000"
console.log(url.hostname); // "example.com"
5. port - The port number as a string (empty string if using default port):
const url = new URL('https://example.com:8080');
console.log(url.port); // "8080"
const url2 = new URL('https://example.com');
console.log(url2.port); // "" (empty string, not "443")
6. pathname - The path section, always starting with /:
const url = new URL('https://example.com/users/123/profile');
console.log(url.pathname); // "/users/123/profile"
url.pathname = '/posts/456';
console.log(url.href); // "https://example.com/posts/456"
7. search - The query string including the ?:
const url = new URL('https://example.com?foo=bar&baz=qux');
console.log(url.search); // "?foo=bar&baz=qux"
8. hash - The fragment identifier including the #:
const url = new URL('https://example.com/page#section-2');
console.log(url.hash); // "#section-2"
9. origin - Read-only property combining protocol, hostname, and port:
const url = new URL('https://example.com:8080/path?query');
console.log(url.origin); // "https://example.com:8080"
Working with Relative URLs
One of the most powerful features of URL() is resolving relative URLs against a base:
// Resolving relative paths
const base = 'https://example.com/users/123/profile';
const url1 = new URL('edit', base);
console.log(url1.href); // "https://example.com/users/123/edit"
const url2 = new URL('../456/posts', base);
console.log(url2.href); // "https://example.com/users/456/posts"
const url3 = new URL('/api/data', base);
console.log(url3.href); // "https://example.com/api/data"
const url4 = new URL('?page=2', base);
console.log(url4.href); // "https://example.com/users/123/profile?page=2"
Key Methods of URL
1. toString() - Returns the complete URL as a string (equivalent to href):
const url = new URL('https://example.com/page');
console.log(url.toString()); // "https://example.com/page"
console.log(String(url)); // "https://example.com/page"
2. toJSON() - Returns the URL string for JSON serialization:
const url = new URL('https://example.com/api');
console.log(JSON.stringify({ endpoint: url }));
// {"endpoint":"https://example.com/api"}
Static Methods
URL.canParse(url, base) - Check if a URL is valid without throwing an error (modern browsers):
console.log(URL.canParse('https://example.com')); // true
console.log(URL.canParse('not a url')); // false
console.log(URL.canParse('/relative', 'https://example.com')); // true
URL.createObjectURL(blob) - Creates a temporary URL for Blob or File objects:
const blob = new Blob(['Hello'], { type: 'text/plain' });
const blobUrl = URL.createObjectURL(blob);
console.log(blobUrl); // "blob:https://example.com/550e8400-e29b-41d4-a716-446655440000"
// Don't forget to revoke when done
URL.revokeObjectURL(blobUrl);
URLSearchParams: The Query String Swiss Army Knife
What is URLSearchParams?
URLSearchParams is an interface for working with query strings. It provides methods to read, add, modify, and delete query parameters without manual string manipulation.
Creating URLSearchParams
There are multiple ways to create a URLSearchParams object:
1. From a query string:
const params = new URLSearchParams('name=John&age=30&age=31');
2. From an object (modern approach):
const params = new URLSearchParams({
name: 'John',
age: 30,
city: 'New York'
});
3. From an array of key-value pairs:
const params = new URLSearchParams([
['name', 'John'],
['age', '30'],
['hobby', 'coding'],
['hobby', 'reading']
]);
4. From a URL object's searchParams property:
const url = new URL('https://example.com?name=John&age=30');
const params = url.searchParams; // Direct reference, not a copy!
Core Methods: Reading Parameters
1. get(name) - Retrieves the first value for a parameter:
const params = new URLSearchParams('name=John&age=30&age=31');
console.log(params.get('name')); // "John"
console.log(params.get('age')); // "30" (first value only)
console.log(params.get('city')); // null (doesn't exist)
2. getAll(name) - Retrieves all values for a parameter as an array:
const params = new URLSearchParams('tag=js&tag=web&tag=api');
console.log(params.getAll('tag')); // ["js", "web", "api"]
console.log(params.getAll('missing')); // [] (empty array)
3. has(name) - Checks if a parameter exists:
const params = new URLSearchParams('name=John&age=30');
console.log(params.has('name')); // true
console.log(params.has('city')); // false
4. keys() - Returns an iterator of all parameter names:
const params = new URLSearchParams('name=John&age=30&city=NYC');
for (const key of params.keys()) {
console.log(key); // "name", "age", "city"
}
// Convert to array
console.log([...params.keys()]); // ["name", "age", "city"]
5. values() - Returns an iterator of all parameter values:
const params = new URLSearchParams('name=John&age=30');
for (const value of params.values()) {
console.log(value); // "John", "30"
}
6. entries() - Returns an iterator of [key, value] pairs:
const params = new URLSearchParams('name=John&age=30');
for (const [key, value] of params.entries()) {
console.log(`${key}: ${value}`);
// "name: John"
// "age: 30"
}
// URLSearchParams is directly iterable
for (const [key, value] of params) {
console.log(`${key}: ${value}`);
}
7. forEach(callback) - Executes a callback for each parameter:
const params = new URLSearchParams('name=John&age=30');
params.forEach((value, key) => {
console.log(`${key} = ${value}`);
});
Core Methods: Modifying Parameters
1. set(name, value) - Sets a parameter, replacing all existing values:
const params = new URLSearchParams('name=John&age=30&age=31');
params.set('age', '25');
console.log(params.toString()); // "name=John&age=25"
params.set('city', 'NYC');
console.log(params.toString()); // "name=John&age=25&city=NYC"
2. append(name, value) - Adds a parameter without removing existing ones:
const params = new URLSearchParams('tag=js');
params.append('tag', 'web');
params.append('tag', 'api');
console.log(params.toString()); // "tag=js&tag=web&tag=api"
3. delete(name) - Removes a parameter:
const params = new URLSearchParams('name=John&age=30&city=NYC');
params.delete('age');
console.log(params.toString()); // "name=John&city=NYC"
Modern delete() with value parameter:
const params = new URLSearchParams('tag=js&tag=web&tag=js');
params.delete('tag', 'js'); // Remove only 'tag=js' entries
console.log(params.toString()); // "tag=web"
4. sort() - Sorts parameters alphabetically by key:
const params = new URLSearchParams('zebra=1&apple=2&mango=3');
params.sort();
console.log(params.toString()); // "apple=2&mango=3&zebra=1"
5. toString() - Converts to query string (without leading ?):
const params = new URLSearchParams({ name: 'John', age: 30 });
console.log(params.toString()); // "name=John&age=30"
console.log('?' + params.toString()); // "?name=John&age=30"
Integration with URL
The real power emerges when combining URL and URLSearchParams:
const url = new URL('https://api.example.com/users');
// url.searchParams is a live URLSearchParams object
url.searchParams.set('page', '2');
url.searchParams.set('limit', '10');
url.searchParams.append('sort', 'name');
console.log(url.href);
// "https://api.example.com/users?page=2&limit=10&sort=name"
// Modifications to searchParams immediately affect the URL
url.searchParams.delete('limit');
console.log(url.href);
// "https://api.example.com/users?page=2&sort=name"
Real-World Problems They Solve
Problem 1: Safe Query String Parsing
Before URL APIs:
// Fragile and error-prone
function getQueryParam(url, param) {
const regex = new RegExp('[?&]' + param + '=([^&#]*)');
const results = regex.exec(url);
return results ? decodeURIComponent(results[1]) : null;
}
// Doesn't handle multiple values, encoding issues, etc.
With URLSearchParams:
const url = new URL(window.location.href);
const paramValue = url.searchParams.get('param');
// Handles encoding, multiple values, and edge cases automatically
Problem 2: Building Dynamic API URLs
Before URL APIs:
// Manual string concatenation - prone to bugs
let apiUrl = 'https://api.example.com/search?';
if (query) apiUrl += 'q=' + encodeURIComponent(query) + '&';
if (page) apiUrl += 'page=' + page + '&';
if (filters) {
filters.forEach(f => {
apiUrl += 'filter=' + encodeURIComponent(f) + '&';
});
}
apiUrl = apiUrl.slice(0, -1); // Remove trailing &
With URL APIs:
const apiUrl = new URL('https://api.example.com/search');
if (query) apiUrl.searchParams.set('q', query);
if (page) apiUrl.searchParams.set('page', page);
if (filters) {
filters.forEach(f => apiUrl.searchParams.append('filter', f));
}
// Clean, readable, no encoding worries
Problem 3: URL Modification Without Reloading
Updating browser URL state:
// Update URL without page reload (for SPAs)
function updateFilters(filters) {
const url = new URL(window.location);
url.searchParams.delete('filter'); // Clear existing
filters.forEach(f => url.searchParams.append('filter', f));
window.history.pushState({}, '', url);
}
Problem 4: Handling Complex Query Strings
Working with arrays and multiple values:
// Building a search URL with multiple filters
const searchUrl = new URL('https://shop.example.com/products');
searchUrl.searchParams.set('category', 'electronics');
searchUrl.searchParams.append('color', 'black');
searchUrl.searchParams.append('color', 'silver');
searchUrl.searchParams.set('minPrice', '100');
searchUrl.searchParams.set('maxPrice', '500');
console.log(searchUrl.href);
// "https://shop.example.com/products?category=electronics&color=black&color=silver&minPrice=100&maxPrice=500"
// Reading multiple values on the backend/client
const colors = searchUrl.searchParams.getAll('color');
console.log(colors); // ["black", "silver"]
Problem 5: Form Data to URL Query
Converting form data to URL query string:
const formData = new FormData(document.querySelector('form'));
const searchParams = new URLSearchParams(formData);
const apiUrl = new URL('https://api.example.com/submit');
apiUrl.search = searchParams.toString();
fetch(apiUrl)
.then(response => response.json())
.then(data => console.log(data));
Problem 6: URL Validation and Sanitization
Safely validating URLs:
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch (err) {
return false;
}
}
console.log(isValidUrl('https://example.com')); // true
console.log(isValidUrl('not a url')); // false
// Or with modern browsers:
console.log(URL.canParse('https://example.com')); // true
Practical Examples
Example 1: Pagination Helper
class PaginationHelper {
constructor(baseUrl) {
this.url = new URL(baseUrl);
}
setPage(page) {
this.url.searchParams.set('page', page);
return this;
}
setLimit(limit) {
this.url.searchParams.set('limit', limit);
return this;
}
setSort(field, order = 'asc') {
this.url.searchParams.set('sort', field);
this.url.searchParams.set('order', order);
return this;
}
getUrl() {
return this.url.href;
}
}
const pagination = new PaginationHelper('https://api.example.com/users');
const url = pagination
.setPage(2)
.setLimit(20)
.setSort('name', 'desc')
.getUrl();
console.log(url);
// "https://api.example.com/users?page=2&limit=20&sort=name&order=desc"
Example 2: Search Filter Manager
class SearchFilters {
constructor(currentUrl = window.location.href) {
this.url = new URL(currentUrl);
}
addFilter(key, value) {
this.url.searchParams.append(key, value);
return this;
}
removeFilter(key, value = null) {
if (value === null) {
this.url.searchParams.delete(key);
} else {
// Remove specific value
const values = this.url.searchParams.getAll(key);
this.url.searchParams.delete(key);
values.filter(v => v !== value).forEach(v => {
this.url.searchParams.append(key, v);
});
}
return this;
}
getFilters(key) {
return this.url.searchParams.getAll(key);
}
clearAll() {
// Keep only specific params like page
const page = this.url.searchParams.get('page');
this.url.search = '';
if (page) this.url.searchParams.set('page', page);
return this;
}
apply() {
window.history.pushState({}, '', this.url);
// Trigger filter update event
window.dispatchEvent(new CustomEvent('filtersChanged'));
}
toString() {
return this.url.href;
}
}
// Usage
const filters = new SearchFilters();
filters
.addFilter('category', 'electronics')
.addFilter('brand', 'apple')
.addFilter('brand', 'samsung')
.apply();
Example 3: API Client with Query Builder
class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
buildUrl(endpoint, params = {}) {
const url = new URL(endpoint, this.baseUrl);
Object.entries(params).forEach(([key, value]) => {
if (Array.isArray(value)) {
value.forEach(v => url.searchParams.append(key, v));
} else if (value !== null && value !== undefined) {
url.searchParams.set(key, value);
}
});
return url.href;
}
async get(endpoint, params = {}) {
const url = this.buildUrl(endpoint, params);
const response = await fetch(url);
return response.json();
}
}
// Usage
const api = new ApiClient('https://api.example.com');
api.get('/products', {
category: 'electronics',
tags: ['new', 'featured'],
minPrice: 100,
maxPrice: 1000,
page: 1
});
// Fetches: https://api.example.com/products?category=electronics&tags=new&tags=featured&minPrice=100&maxPrice=1000&page=1
Example 4: URL State Synchronizer
class UrlStateSync {
constructor() {
this.url = new URL(window.location);
this.listeners = new Map();
}
setState(key, value) {
if (value === null || value === undefined || value === '') {
this.url.searchParams.delete(key);
} else {
this.url.searchParams.set(key, value);
}
this.updateUrl();
this.notifyListeners(key, value);
}
getState(key) {
return this.url.searchParams.get(key);
}
updateUrl() {
window.history.replaceState({}, '', this.url);
}
onChange(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(callback);
}
notifyListeners(key, value) {
if (this.listeners.has(key)) {
this.listeners.get(key).forEach(callback => callback(value));
}
}
}
// Usage
const urlState = new UrlStateSync();
urlState.onChange('theme', (theme) => {
document.body.className = theme;
});
urlState.setState('theme', 'dark'); // Updates URL and applies theme
Common Pitfalls and Best Practices
Pitfall 1: Forgetting URL Encoding
// ❌ Wrong - manual encoding issues
const url = 'https://api.example.com/search?q=' + query;
// ✅ Correct - automatic encoding
const url = new URL('https://api.example.com/search');
url.searchParams.set('q', query);
Pitfall 2: Modifying searchParams Reference
const url = new URL('https://example.com?name=John');
// url.searchParams is a live reference
const params = url.searchParams;
params.set('age', '30');
console.log(url.href); // Changed! "https://example.com?name=John&age=30"
// To get an independent copy:
const paramsCopy = new URLSearchParams(url.searchParams);
Pitfall 3: Using set() vs append()
const params = new URLSearchParams();
// set() replaces all values
params.set('tag', 'js');
params.set('tag', 'web');
console.log(params.toString()); // "tag=web" (only one)
// append() adds multiple values
params.append('tag', 'js');
params.append('tag', 'web');
console.log(params.toString()); // "tag=js&tag=web"
Best Practice 1: Always Validate URLs
function safeUrlParse(urlString, base) {
try {
return new URL(urlString, base);
} catch (error) {
console.error('Invalid URL:', urlString);
return null;
}
}
Best Practice 2: Use searchParams for Query Manipulation
// ❌ Avoid direct search manipulation
url.search = '?name=John&age=30';
// ✅ Use searchParams
url.searchParams.set('name', 'John');
url.searchParams.set('age', '30');
Best Practice 3: Clone URLs When Needed
function modifyUrlWithoutMutating(originalUrl, modifications) {
const url = new URL(originalUrl.href); // Create new instance
Object.entries(modifications).forEach(([key, value]) => {
url.searchParams.set(key, value);
});
return url;
}
Browser Support and Polyfills
The URL() and URLSearchParams APIs have excellent browser support:
- URL: Supported in all modern browsers (Chrome 32+, Firefox 26+, Safari 7+, Edge 12+)
- URLSearchParams: Supported in all modern browsers (Chrome 49+, Firefox 44+, Safari 10.3+, Edge 17+)
For older browsers, you can use the url-polyfill package:
import 'url-polyfill';
// Now URL and URLSearchParams work in older browsers
Conclusion
The URL() and URLSearchParams APIs represent a massive improvement over manual URL string manipulation. They provide:
- Type safety: Automatic encoding and validation
- Simplicity: Intuitive methods for common operations
- Reliability: Handle edge cases correctly
- Maintainability: Self-documenting code
- Standards compliance: Following web standards
Whether you're building SPAs, working with APIs, managing client-side routing, or handling form submissions, these APIs should be your go-to tools for URL manipulation.
Take Your JavaScript Skills Further
If you found this deep dive valuable, you'll love what we're building at TekBreed – a comprehensive learning platform designed specifically for software engineers who want to master modern web development.
At TekBreed, we create in-depth, practical courses that go beyond surface-level tutorials. We focus on the "why" behind the code, real-world applications, and the tiny details that separate good developers from great ones.
🚀 The waitlist is now live at tekbreed.com
We're currently in active development and planning to launch in Q1 2026. Join the waitlist to:
- Get early access to our platform
- Receive exclusive content and tutorials
- Help shape the courses we create
- Be part of a community of passionate engineers
We're building something special for developers who refuse to settle for shallow understanding. See you at TekBreed! 🎯
Top comments (0)