Web APIs
JavaScript URL API: Complete URL Manipulation Guide
Master the URL API in JavaScript for parsing and manipulating URLs. Learn URL components, search params, and advanced URL handling techniques.
By JavaScriptDoc Team•
urlparsingparametersnavigationjavascript
JavaScript URL API: Complete URL Manipulation Guide
The URL API provides a powerful interface for working with URLs, making it easy to parse, construct, and manipulate URLs and their components in JavaScript applications.
Understanding the URL API
The URL interface represents an object providing static methods used for creating object URLs, along with properties for accessing URL components.
// Create a URL object
const url = new URL(
'https://example.com:8080/path/to/page?name=John&age=30#section'
);
// Access URL components
console.log({
href: url.href, // Full URL
protocol: url.protocol, // "https:"
host: url.host, // "example.com:8080"
hostname: url.hostname, // "example.com"
port: url.port, // "8080"
pathname: url.pathname, // "/path/to/page"
search: url.search, // "?name=John&age=30"
hash: url.hash, // "#section"
origin: url.origin, // "https://example.com:8080"
});
// Modify URL components
url.pathname = '/new/path';
url.port = '3000';
console.log(url.href); // Updated URL
// Create relative URLs with base
const base = 'https://example.com/docs/';
const relative = new URL('../api/users', base);
console.log(relative.href); // "https://example.com/api/users"
URL Construction and Parsing
Advanced URL Manager
class URLManager {
constructor(baseURL = null) {
this.baseURL = baseURL;
this.cache = new Map();
}
// Parse URL safely
parseURL(urlString, base = this.baseURL) {
try {
return new URL(urlString, base);
} catch (error) {
console.error('Invalid URL:', urlString, error);
return null;
}
}
// Build URL from components
buildURL(components = {}) {
const {
protocol = 'https:',
hostname = 'localhost',
port = '',
pathname = '/',
search = '',
hash = '',
username = '',
password = '',
} = components;
try {
const url = new URL(`${protocol}//${hostname}`);
if (port) url.port = port;
if (pathname) url.pathname = pathname;
if (search) url.search = search;
if (hash) url.hash = hash;
if (username) url.username = username;
if (password) url.password = password;
return url;
} catch (error) {
console.error('Failed to build URL:', error);
return null;
}
}
// Validate URL
isValidURL(urlString, options = {}) {
const {
protocols = ['http:', 'https:'],
requireProtocol = true,
allowLocalhost = true,
allowIP = true,
} = options;
try {
const url = new URL(urlString);
// Check protocol
if (requireProtocol && !protocols.includes(url.protocol)) {
return false;
}
// Check hostname
if (!allowLocalhost && url.hostname === 'localhost') {
return false;
}
// Check IP addresses
if (!allowIP && this.isIPAddress(url.hostname)) {
return false;
}
return true;
} catch {
return false;
}
}
// Check if hostname is IP address
isIPAddress(hostname) {
// IPv4 pattern
const ipv4Pattern = /^(\d{1,3}\.){3}\d{1,3}$/;
// Simplified IPv6 pattern
const ipv6Pattern = /^([\da-f]{1,4}:){7}[\da-f]{1,4}$/i;
return ipv4Pattern.test(hostname) || ipv6Pattern.test(hostname);
}
// Normalize URL
normalizeURL(urlString) {
try {
const url = new URL(urlString);
// Remove default ports
if (
(url.protocol === 'http:' && url.port === '80') ||
(url.protocol === 'https:' && url.port === '443')
) {
url.port = '';
}
// Ensure trailing slash for root path
if (url.pathname === '') {
url.pathname = '/';
}
// Sort search params
const params = new URLSearchParams(url.search);
const sortedParams = new URLSearchParams(
[...params].sort(([a], [b]) => a.localeCompare(b))
);
url.search = sortedParams.toString();
// Remove empty hash
if (url.hash === '#') {
url.hash = '';
}
return url.href;
} catch (error) {
console.error('Failed to normalize URL:', error);
return urlString;
}
}
// Compare URLs
compareURLs(url1, url2, options = {}) {
const {
ignoreProtocol = false,
ignoreWWW = false,
ignoreCase = false,
ignoreSearch = false,
ignoreHash = false,
ignoreTrailingSlash = false,
} = options;
try {
let normalized1 = this.normalizeURL(url1);
let normalized2 = this.normalizeURL(url2);
const urlObj1 = new URL(normalized1);
const urlObj2 = new URL(normalized2);
// Apply ignore options
if (ignoreProtocol) {
urlObj1.protocol = urlObj2.protocol = 'https:';
}
if (ignoreWWW) {
urlObj1.hostname = urlObj1.hostname.replace(/^www\./, '');
urlObj2.hostname = urlObj2.hostname.replace(/^www\./, '');
}
if (ignoreSearch) {
urlObj1.search = urlObj2.search = '';
}
if (ignoreHash) {
urlObj1.hash = urlObj2.hash = '';
}
if (ignoreTrailingSlash) {
urlObj1.pathname = urlObj1.pathname.replace(/\/$/, '');
urlObj2.pathname = urlObj2.pathname.replace(/\/$/, '');
}
normalized1 = urlObj1.href;
normalized2 = urlObj2.href;
if (ignoreCase) {
normalized1 = normalized1.toLowerCase();
normalized2 = normalized2.toLowerCase();
}
return normalized1 === normalized2;
} catch (error) {
console.error('Failed to compare URLs:', error);
return false;
}
}
// Extract domain info
extractDomainInfo(urlString) {
try {
const url = new URL(urlString);
const parts = url.hostname.split('.');
let domain, subdomain, tld;
if (parts.length >= 3) {
tld = parts[parts.length - 1];
domain = parts[parts.length - 2];
subdomain = parts.slice(0, -2).join('.');
} else if (parts.length === 2) {
tld = parts[1];
domain = parts[0];
subdomain = '';
} else {
domain = url.hostname;
subdomain = '';
tld = '';
}
return {
hostname: url.hostname,
domain: domain + (tld ? '.' + tld : ''),
subdomain,
tld,
isWWW: subdomain === 'www',
isIP: this.isIPAddress(url.hostname),
};
} catch (error) {
console.error('Failed to extract domain info:', error);
return null;
}
}
// Join URL paths
joinPaths(...paths) {
return paths
.map((path, index) => {
// Remove leading slash from all but first path
if (index > 0) {
path = path.replace(/^\/+/, '');
}
// Remove trailing slash from all but last path
if (index < paths.length - 1) {
path = path.replace(/\/+$/, '');
}
return path;
})
.join('/');
}
// Resolve URL path
resolvePath(base, relative) {
try {
const baseURL = new URL(base);
const resolved = new URL(relative, baseURL);
return resolved.href;
} catch (error) {
console.error('Failed to resolve path:', error);
return null;
}
}
// Get URL without query params
getURLWithoutParams(urlString) {
try {
const url = new URL(urlString);
url.search = '';
return url.href;
} catch (error) {
console.error('Failed to remove params:', error);
return urlString;
}
}
// Cache URL objects
getCachedURL(urlString) {
if (!this.cache.has(urlString)) {
const url = this.parseURL(urlString);
if (url) {
this.cache.set(urlString, url);
}
}
return this.cache.get(urlString);
}
// Clear cache
clearCache() {
this.cache.clear();
}
}
// Usage
const urlManager = new URLManager('https://api.example.com');
// Parse URL
const url = urlManager.parseURL('/users/123', 'https://api.example.com');
console.log(url?.href); // "https://api.example.com/users/123"
// Build URL
const built = urlManager.buildURL({
protocol: 'https:',
hostname: 'example.com',
pathname: '/api/users',
search: '?active=true',
});
console.log(built?.href);
// Validate URL
console.log(urlManager.isValidURL('https://example.com')); // true
console.log(urlManager.isValidURL('ftp://example.com')); // false (not in allowed protocols)
// Compare URLs
console.log(
urlManager.compareURLs(
'https://www.example.com/path/',
'http://example.com/path',
{ ignoreProtocol: true, ignoreWWW: true, ignoreTrailingSlash: true }
)
); // true
// Extract domain info
const domainInfo = urlManager.extractDomainInfo(
'https://blog.example.com/posts'
);
console.log(domainInfo);
// { hostname: 'blog.example.com', domain: 'example.com', subdomain: 'blog', ... }
URLSearchParams Handling
Search Parameters Manager
class SearchParamsManager {
constructor(initialParams = null) {
this.params =
initialParams instanceof URLSearchParams
? new URLSearchParams(initialParams)
: new URLSearchParams(initialParams || '');
}
// Get parameter value(s)
get(key, options = {}) {
const { multiple = false, defaultValue = null, parse = false } = options;
if (multiple) {
const values = this.params.getAll(key);
return values.length > 0 ? values : defaultValue;
}
const value = this.params.get(key);
if (value === null) {
return defaultValue;
}
if (parse) {
return this.parseValue(value);
}
return value;
}
// Set parameter value(s)
set(key, value, options = {}) {
const { append = false } = options;
if (value === null || value === undefined) {
this.params.delete(key);
return this;
}
if (Array.isArray(value)) {
if (!append) {
this.params.delete(key);
}
value.forEach((v) => this.params.append(key, String(v)));
} else if (typeof value === 'object') {
this.params.set(key, JSON.stringify(value));
} else {
if (append) {
this.params.append(key, String(value));
} else {
this.params.set(key, String(value));
}
}
return this;
}
// Update multiple parameters
update(updates, options = {}) {
const { merge = true } = options;
if (!merge) {
this.params = new URLSearchParams();
}
Object.entries(updates).forEach(([key, value]) => {
this.set(key, value);
});
return this;
}
// Remove parameters
remove(...keys) {
keys.forEach((key) => this.params.delete(key));
return this;
}
// Check if parameter exists
has(key, value = null) {
if (value === null) {
return this.params.has(key);
}
const values = this.params.getAll(key);
return values.includes(String(value));
}
// Filter parameters
filter(predicate) {
const filtered = new URLSearchParams();
for (const [key, value] of this.params) {
if (predicate(key, value)) {
filtered.append(key, value);
}
}
this.params = filtered;
return this;
}
// Map parameters
map(transformer) {
const mapped = new URLSearchParams();
for (const [key, value] of this.params) {
const [newKey, newValue] = transformer(key, value);
if (newKey !== null && newValue !== null) {
mapped.append(newKey, newValue);
}
}
this.params = mapped;
return this;
}
// Sort parameters
sort(compareFn) {
const entries = [...this.params];
if (compareFn) {
entries.sort(([aKey, aVal], [bKey, bVal]) =>
compareFn(aKey, aVal, bKey, bVal)
);
} else {
entries.sort(([a], [b]) => a.localeCompare(b));
}
this.params = new URLSearchParams(entries);
return this;
}
// Parse value based on content
parseValue(value) {
// Boolean
if (value === 'true') return true;
if (value === 'false') return false;
// Number
if (/^-?\d+$/.test(value)) {
return parseInt(value, 10);
}
if (/^-?\d*\.\d+$/.test(value)) {
return parseFloat(value);
}
// JSON
if (
(value.startsWith('{') && value.endsWith('}')) ||
(value.startsWith('[') && value.endsWith(']'))
) {
try {
return JSON.parse(value);
} catch {
return value;
}
}
// Date
if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
const date = new Date(value);
if (!isNaN(date.getTime())) {
return date;
}
}
return value;
}
// Convert to different formats
toObject(options = {}) {
const { parse = false, groupArrays = true } = options;
const result = {};
for (const [key, value] of this.params) {
const parsedValue = parse ? this.parseValue(value) : value;
if (groupArrays && key in result) {
if (!Array.isArray(result[key])) {
result[key] = [result[key]];
}
result[key].push(parsedValue);
} else {
result[key] = parsedValue;
}
}
return result;
}
// Convert to query string
toString() {
return this.params.toString();
}
// Convert to form data
toFormData() {
const formData = new FormData();
for (const [key, value] of this.params) {
formData.append(key, value);
}
return formData;
}
// Clone parameters
clone() {
return new SearchParamsManager(new URLSearchParams(this.params));
}
// Get all keys
keys() {
return [...new Set(this.params.keys())];
}
// Get all values
values() {
return [...this.params.values()];
}
// Get entries
entries() {
return [...this.params.entries()];
}
// Clear all parameters
clear() {
this.params = new URLSearchParams();
return this;
}
// Count parameters
get size() {
return [...this.params].length;
}
// Count unique keys
get uniqueKeys() {
return new Set(this.params.keys()).size;
}
}
// Usage
const searchParams = new SearchParamsManager(
'?name=John&age=30&hobbies=reading&hobbies=gaming'
);
// Get values
console.log(searchParams.get('name')); // "John"
console.log(searchParams.get('hobbies', { multiple: true })); // ["reading", "gaming"]
console.log(searchParams.get('age', { parse: true })); // 30 (number)
// Set values
searchParams
.set('email', 'john@example.com')
.set('tags', ['javascript', 'web'])
.update({ active: true, role: 'admin' });
// Filter parameters
searchParams.filter((key, value) => key !== 'age');
// Sort parameters
searchParams.sort();
// Convert to object
const obj = searchParams.toObject({ parse: true });
console.log(obj);
// Build URL with params
const baseURL = 'https://api.example.com/users';
const urlWithParams = `${baseURL}?${searchParams.toString()}`;
console.log(urlWithParams);
URL Routing
URL Router Implementation
class URLRouter {
constructor(options = {}) {
this.routes = new Map();
this.middlewares = [];
this.notFoundHandler = null;
this.errorHandler = null;
this.baseURL = options.baseURL || window.location.origin;
this.init();
}
// Initialize router
init() {
// Listen for popstate events
window.addEventListener('popstate', (event) => {
this.handleRoute(window.location.pathname, event.state);
});
// Intercept link clicks
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href]');
if (link && this.shouldIntercept(link)) {
event.preventDefault();
this.navigate(link.href);
}
});
}
// Add route
route(pattern, handler, options = {}) {
const route = {
pattern: this.patternToRegex(pattern),
handler,
options,
params: this.extractParamNames(pattern),
};
this.routes.set(pattern, route);
return this;
}
// Convert route pattern to regex
patternToRegex(pattern) {
const escaped = pattern.replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
const withParams = escaped.replace(/:(\w+)/g, '(?<$1>[^/]+)');
const withWildcard = withParams.replace(/\*/g, '.*');
return new RegExp(`^${withWildcard}$`);
}
// Extract parameter names from pattern
extractParamNames(pattern) {
const matches = pattern.matchAll(/:(\w+)/g);
return [...matches].map((match) => match[1]);
}
// Add middleware
use(middleware) {
this.middlewares.push(middleware);
return this;
}
// Navigate to URL
async navigate(url, options = {}) {
const { replace = false, state = {}, trigger = true } = options;
try {
const urlObj = new URL(url, this.baseURL);
const path = urlObj.pathname;
// Update browser history
if (replace) {
window.history.replaceState(state, '', url);
} else {
window.history.pushState(state, '', url);
}
// Trigger route handler
if (trigger) {
await this.handleRoute(path, state);
}
} catch (error) {
console.error('Navigation error:', error);
if (this.errorHandler) {
this.errorHandler(error);
}
}
}
// Handle route
async handleRoute(path, state = {}) {
const url = new URL(path, this.baseURL);
const context = {
url,
path: url.pathname,
params: {},
query: new SearchParamsManager(url.search),
state,
next: null,
};
// Find matching route
let matchedRoute = null;
for (const [pattern, route] of this.routes) {
const match = url.pathname.match(route.pattern);
if (match) {
context.params = match.groups || {};
matchedRoute = route;
break;
}
}
// Run middlewares
const middlewareChain = [...this.middlewares];
const runMiddleware = async (index = 0) => {
if (index >= middlewareChain.length) {
// Run route handler
if (matchedRoute) {
await matchedRoute.handler(context);
} else if (this.notFoundHandler) {
await this.notFoundHandler(context);
} else {
console.error('No route found for:', path);
}
return;
}
const middleware = middlewareChain[index];
await middleware(context, () => runMiddleware(index + 1));
};
await runMiddleware();
}
// Should intercept link
shouldIntercept(link) {
// Only intercept internal links
const href = link.getAttribute('href');
if (!href || href.startsWith('#')) return false;
try {
const url = new URL(href, this.baseURL);
return (
url.origin === new URL(this.baseURL).origin &&
!link.hasAttribute('download') &&
link.target !== '_blank' &&
!link.classList.contains('external')
);
} catch {
return false;
}
}
// Set not found handler
notFound(handler) {
this.notFoundHandler = handler;
return this;
}
// Set error handler
error(handler) {
this.errorHandler = handler;
return this;
}
// Get current route info
getCurrentRoute() {
const url = new URL(window.location.href);
for (const [pattern, route] of this.routes) {
const match = url.pathname.match(route.pattern);
if (match) {
return {
pattern,
params: match.groups || {},
query: new SearchParamsManager(url.search),
};
}
}
return null;
}
// Generate URL from route
generateURL(pattern, params = {}, query = {}) {
let url = pattern;
// Replace parameters
Object.entries(params).forEach(([key, value]) => {
url = url.replace(`:${key}`, value);
});
// Add query parameters
const searchParams = new SearchParamsManager();
searchParams.update(query);
const queryString = searchParams.toString();
if (queryString) {
url += '?' + queryString;
}
return url;
}
// Check if route is active
isActive(pattern, exact = false) {
const current = window.location.pathname;
if (exact) {
return current === pattern;
}
return current.startsWith(pattern);
}
}
// Usage
const router = new URLRouter({ baseURL: 'https://app.example.com' });
// Add middleware
router.use(async (context, next) => {
console.log('Navigating to:', context.path);
// Check authentication
if (context.path.startsWith('/admin') && !isAuthenticated()) {
router.navigate('/login');
return;
}
await next();
});
// Define routes
router
.route('/', async (context) => {
console.log('Home page');
document.getElementById('content').innerHTML = '<h1>Home</h1>';
})
.route('/users', async (context) => {
const { page = 1 } = context.query.toObject();
console.log('Users list, page:', page);
})
.route('/users/:id', async (context) => {
const { id } = context.params;
console.log('User details:', id);
})
.route('/posts/:category/:id', async (context) => {
const { category, id } = context.params;
console.log('Post:', category, id);
})
.notFound(async (context) => {
console.log('404 - Not found:', context.path);
document.getElementById('content').innerHTML =
'<h1>404 - Page Not Found</h1>';
})
.error((error) => {
console.error('Router error:', error);
});
// Navigate programmatically
router.navigate('/users/123', { state: { from: 'list' } });
// Generate URLs
const userUrl = router.generateURL(
'/users/:id',
{ id: 456 },
{ tab: 'profile' }
);
console.log(userUrl); // "/users/456?tab=profile"
// Check active route
console.log(router.isActive('/users')); // true if on /users/*
URL Utilities
URL Helper Functions
class URLUtilities {
// Extract file extension from URL
static getFileExtension(urlString) {
try {
const url = new URL(urlString);
const pathname = url.pathname;
const lastDot = pathname.lastIndexOf('.');
const lastSlash = pathname.lastIndexOf('/');
if (lastDot > lastSlash && lastDot !== pathname.length - 1) {
return pathname.substring(lastDot + 1).toLowerCase();
}
return '';
} catch {
return '';
}
}
// Get filename from URL
static getFilename(urlString, includeExtension = true) {
try {
const url = new URL(urlString);
const pathname = url.pathname;
const filename = pathname.substring(pathname.lastIndexOf('/') + 1);
if (!includeExtension) {
const dotIndex = filename.lastIndexOf('.');
if (dotIndex > 0) {
return filename.substring(0, dotIndex);
}
}
return filename;
} catch {
return '';
}
}
// Check if URL is absolute
static isAbsoluteURL(urlString) {
try {
new URL(urlString);
return true;
} catch {
return false;
}
}
// Check if URL is external
static isExternalURL(urlString, baseURL = window.location.origin) {
try {
const url = new URL(urlString, baseURL);
const base = new URL(baseURL);
return url.origin !== base.origin;
} catch {
return false;
}
}
// Add or update query parameter
static addQueryParam(urlString, key, value) {
try {
const url = new URL(urlString);
url.searchParams.set(key, value);
return url.href;
} catch {
return urlString;
}
}
// Remove query parameter
static removeQueryParam(urlString, key) {
try {
const url = new URL(urlString);
url.searchParams.delete(key);
return url.href;
} catch {
return urlString;
}
}
// Get all query parameters as object
static getQueryParams(urlString) {
try {
const url = new URL(urlString);
const params = {};
for (const [key, value] of url.searchParams) {
if (key in params) {
if (!Array.isArray(params[key])) {
params[key] = [params[key]];
}
params[key].push(value);
} else {
params[key] = value;
}
}
return params;
} catch {
return {};
}
}
// Create data URL
static createDataURL(data, mimeType = 'text/plain') {
const base64 = btoa(unescape(encodeURIComponent(data)));
return `data:${mimeType};base64,${base64}`;
}
// Parse data URL
static parseDataURL(dataURL) {
const match = dataURL.match(/^data:([^;]+);base64,(.+)$/);
if (match) {
const [, mimeType, base64] = match;
const data = decodeURIComponent(escape(atob(base64)));
return {
mimeType,
data,
base64,
};
}
return null;
}
// Create blob URL
static createBlobURL(data, type = 'text/plain') {
const blob = new Blob([data], { type });
return URL.createObjectURL(blob);
}
// Revoke blob URL
static revokeBlobURL(url) {
URL.revokeObjectURL(url);
}
// Encode URL component safely
static encodeURLComponent(str) {
return encodeURIComponent(str).replace(
/[!'()*]/g,
(c) => '%' + c.charCodeAt(0).toString(16)
);
}
// Build URL with template
static buildURLFromTemplate(template, params = {}, query = {}) {
let url = template;
// Replace path parameters
Object.entries(params).forEach(([key, value]) => {
url = url.replace(`{${key}}`, encodeURIComponent(value));
});
// Add query parameters
const queryString = new URLSearchParams(query).toString();
if (queryString) {
url += (url.includes('?') ? '&' : '?') + queryString;
}
return url;
}
// Parse URL template
static parseURLTemplate(template, url) {
const templateParts = template.split(/[?#]/)[0].split('/');
const urlParts = url.split(/[?#]/)[0].split('/');
if (templateParts.length !== urlParts.length) {
return null;
}
const params = {};
for (let i = 0; i < templateParts.length; i++) {
const templatePart = templateParts[i];
const urlPart = urlParts[i];
if (templatePart.startsWith('{') && templatePart.endsWith('}')) {
const paramName = templatePart.slice(1, -1);
params[paramName] = decodeURIComponent(urlPart);
} else if (templatePart !== urlPart) {
return null;
}
}
return params;
}
// Shorten URL (simple hash)
static async shortenURL(longURL) {
const encoder = new TextEncoder();
const data = encoder.encode(longURL);
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((b) => b.toString(16).padStart(2, '0'))
.join('');
return hashHex.substring(0, 8); // First 8 characters
}
// Clean URL (remove tracking parameters)
static cleanURL(
urlString,
removeParams = [
'utm_source',
'utm_medium',
'utm_campaign',
'utm_term',
'utm_content',
'fbclid',
'gclid',
]
) {
try {
const url = new URL(urlString);
removeParams.forEach((param) => {
url.searchParams.delete(param);
});
return url.href;
} catch {
return urlString;
}
}
}
// Usage examples
console.log(URLUtilities.getFileExtension('https://example.com/file.pdf')); // "pdf"
console.log(URLUtilities.getFilename('https://example.com/docs/guide.html')); // "guide.html"
console.log(URLUtilities.isAbsoluteURL('/path/to/page')); // false
console.log(URLUtilities.isExternalURL('https://other.com/page')); // true
// Add query parameter
const urlWithParam = URLUtilities.addQueryParam(
'https://example.com/page',
'sort',
'date'
);
console.log(urlWithParam); // "https://example.com/page?sort=date"
// Build from template
const apiURL = URLUtilities.buildURLFromTemplate(
'https://api.example.com/users/{userId}/posts/{postId}',
{ userId: '123', postId: '456' },
{ include: 'comments', limit: 10 }
);
console.log(apiURL);
// Clean tracking parameters
const cleanedURL = URLUtilities.cleanURL(
'https://example.com/page?utm_source=newsletter&page=1'
);
console.log(cleanedURL); // "https://example.com/page?page=1"
URL State Management
URL-Based State Manager
class URLStateManager {
constructor(options = {}) {
this.options = {
prefix: 'state_',
encode: true,
debounce: 100,
...options,
};
this.state = {};
this.listeners = new Map();
this.updateTimeout = null;
this.init();
}
// Initialize state from URL
init() {
this.loadStateFromURL();
// Listen for popstate events
window.addEventListener('popstate', () => {
this.loadStateFromURL();
this.notifyListeners();
});
}
// Load state from URL
loadStateFromURL() {
const params = new URLSearchParams(window.location.search);
this.state = {};
for (const [key, value] of params) {
if (key.startsWith(this.options.prefix)) {
const stateKey = key.substring(this.options.prefix.length);
this.state[stateKey] = this.decodeValue(value);
}
}
}
// Save state to URL
saveStateToURL(replace = false) {
clearTimeout(this.updateTimeout);
this.updateTimeout = setTimeout(() => {
const url = new URL(window.location.href);
// Remove existing state params
[...url.searchParams.keys()].forEach((key) => {
if (key.startsWith(this.options.prefix)) {
url.searchParams.delete(key);
}
});
// Add current state
Object.entries(this.state).forEach(([key, value]) => {
if (value !== null && value !== undefined) {
const paramKey = this.options.prefix + key;
const encodedValue = this.encodeValue(value);
url.searchParams.set(paramKey, encodedValue);
}
});
// Update URL
const method = replace ? 'replaceState' : 'pushState';
window.history[method](null, '', url.href);
}, this.options.debounce);
}
// Get state value
get(key, defaultValue = null) {
return this.state[key] !== undefined ? this.state[key] : defaultValue;
}
// Set state value
set(key, value, options = {}) {
const { silent = false, replace = false } = options;
if (this.state[key] === value) return;
const oldValue = this.state[key];
this.state[key] = value;
if (!silent) {
this.saveStateToURL(replace);
this.notifyListeners(key, value, oldValue);
}
}
// Update multiple state values
update(updates, options = {}) {
const { silent = false, replace = false } = options;
const changes = [];
Object.entries(updates).forEach(([key, value]) => {
if (this.state[key] !== value) {
const oldValue = this.state[key];
this.state[key] = value;
changes.push({ key, value, oldValue });
}
});
if (changes.length > 0 && !silent) {
this.saveStateToURL(replace);
changes.forEach(({ key, value, oldValue }) => {
this.notifyListeners(key, value, oldValue);
});
}
}
// Remove state value
remove(key, options = {}) {
const { silent = false, replace = true } = options;
if (!(key in this.state)) return;
const oldValue = this.state[key];
delete this.state[key];
if (!silent) {
this.saveStateToURL(replace);
this.notifyListeners(key, undefined, oldValue);
}
}
// Clear all state
clear(options = {}) {
const { silent = false, replace = true } = options;
this.state = {};
if (!silent) {
this.saveStateToURL(replace);
this.notifyListeners();
}
}
// Subscribe to state changes
subscribe(key, callback) {
if (!this.listeners.has(key)) {
this.listeners.set(key, new Set());
}
this.listeners.get(key).add(callback);
// Call with current value
callback(this.get(key), undefined);
// Return unsubscribe function
return () => {
const callbacks = this.listeners.get(key);
if (callbacks) {
callbacks.delete(callback);
if (callbacks.size === 0) {
this.listeners.delete(key);
}
}
};
}
// Notify listeners
notifyListeners(key = null, value = undefined, oldValue = undefined) {
if (key) {
// Notify specific key listeners
const callbacks = this.listeners.get(key);
if (callbacks) {
callbacks.forEach((callback) => callback(value, oldValue));
}
} else {
// Notify all listeners
this.listeners.forEach((callbacks, key) => {
const value = this.get(key);
callbacks.forEach((callback) => callback(value, undefined));
});
}
// Notify wildcard listeners
const wildcardCallbacks = this.listeners.get('*');
if (wildcardCallbacks) {
wildcardCallbacks.forEach((callback) =>
callback(this.state, key ? { [key]: oldValue } : {})
);
}
}
// Encode value for URL
encodeValue(value) {
if (!this.options.encode) {
return String(value);
}
if (typeof value === 'object') {
return btoa(JSON.stringify(value));
}
return String(value);
}
// Decode value from URL
decodeValue(value) {
if (!this.options.encode) {
return value;
}
// Try to decode as base64 JSON
try {
const decoded = atob(value);
return JSON.parse(decoded);
} catch {
// Not encoded, parse as regular value
if (value === 'true') return true;
if (value === 'false') return false;
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
return value;
}
}
// Get shareable URL
getShareableURL(includeKeys = null) {
const url = new URL(window.location.href);
// Clear existing params
url.search = '';
// Add selected state
const keysToInclude = includeKeys || Object.keys(this.state);
keysToInclude.forEach((key) => {
if (key in this.state && this.state[key] != null) {
const paramKey = this.options.prefix + key;
const encodedValue = this.encodeValue(this.state[key]);
url.searchParams.set(paramKey, encodedValue);
}
});
return url.href;
}
// Export state
exportState() {
return { ...this.state };
}
// Import state
importState(state, options = {}) {
this.update(state, options);
}
}
// Usage
const urlState = new URLStateManager({ prefix: 's_' });
// Subscribe to state changes
urlState.subscribe('filter', (value, oldValue) => {
console.log('Filter changed:', oldValue, '->', value);
updateFilterUI(value);
});
urlState.subscribe('page', (value) => {
console.log('Page changed to:', value);
loadPage(value);
});
// Update state (will update URL)
urlState.set('filter', 'active');
urlState.set('page', 2);
// Update multiple values
urlState.update({
sort: 'date',
order: 'desc',
view: 'grid',
});
// Get state values
const currentFilter = urlState.get('filter', 'all');
const currentPage = urlState.get('page', 1);
// Get shareable URL
const shareURL = urlState.getShareableURL(['filter', 'sort']);
console.log('Share this URL:', shareURL);
// Clear specific state
urlState.remove('page');
// Export/Import
const exported = urlState.exportState();
urlState.importState(exported);
Best Practices
-
Always validate URLs
try { const url = new URL(userInput); // URL is valid } catch { // Invalid URL }
-
Use URLSearchParams for query strings
const params = new URLSearchParams(window.location.search); params.set('key', 'value');
-
Handle relative URLs properly
const base = 'https://example.com/docs/'; const url = new URL('../api', base); // Resolves correctly
-
Clean up blob URLs
const blobURL = URL.createObjectURL(blob); // Use URL... URL.revokeObjectURL(blobURL);
Conclusion
The URL API provides powerful tools for URL manipulation:
- URL parsing and construction
- Search parameters handling
- URL routing for SPAs
- State management via URLs
- URL utilities for common tasks
- Cross-browser compatibility
Key takeaways:
- Use URL constructor for safe parsing
- Leverage URLSearchParams for query strings
- Build robust routing systems
- Manage application state in URLs
- Validate and sanitize URL inputs
- Consider URL length limitations
Master the URL API to build modern web applications with clean, shareable URLs!