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

  1. Always validate URLs

    try {
      const url = new URL(userInput);
      // URL is valid
    } catch {
      // Invalid URL
    }
    
  2. Use URLSearchParams for query strings

    const params = new URLSearchParams(window.location.search);
    params.set('key', 'value');
    
  3. Handle relative URLs properly

    const base = 'https://example.com/docs/';
    const url = new URL('../api', base); // Resolves correctly
    
  4. 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!