DEV Community

SEN LLC
SEN LLC

Posted on

A URL Parser and Query String Editor With Live Rebuild

A URL Parser and Query String Editor With Live Rebuild

Paste a URL, see it decomposed into protocol, host, port, path, query params, and hash. Edit any piece — or add/remove/update query params in a table — and the URL rebuilds live. Plus a percent-encoding panel for when you need to manually encode a value.

Working with URLs is surprisingly finicky. Query params with special characters need encoding. Repeated keys (like ?tag=a&tag=b) need array handling. Editing query params by string manipulation is error-prone. A structured editor removes the guesswork.

🔗 Live demo: https://sen.ltd/portfolio/url-parser/
📦 GitHub: https://github.com/sen-ltd/url-parser

Screenshot

Features:

  • Parse URL into components
  • Live-edit each component (protocol, host, port, path, hash)
  • Query param table editor (add/remove/update/reorder)
  • Percent encode/decode tools
  • URL validation
  • Handles IPv6, repeated keys, special chars
  • Japanese / English UI
  • Zero dependencies, 66 tests

Wrapping the URL API

Modern browsers have a built-in URL class, but it's clunky for interactive editing:

export function parseURL(str) {
  try {
    const u = new URL(str);
    return {
      protocol: u.protocol,
      username: u.username,
      password: u.password,
      host: u.hostname,
      port: u.port,
      pathname: u.pathname,
      search: u.search,
      hash: u.hash,
      queryParams: parseQuery(u.search),
    };
  } catch {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

The parsed object is flat and easy to edit. The queryParams key is an array of pairs instead of an object — that's important, because objects can't represent repeated keys (?tag=a&tag=b), which are valid URL syntax.

Query params as ordered pairs

export function parseQuery(search) {
  if (!search || search === '?') return [];
  const str = search.startsWith('?') ? search.slice(1) : search;
  return str.split('&').map(pair => {
    const [key, ...rest] = pair.split('=');
    return [decodeComponent(key), decodeComponent(rest.join('='))];
  });
}

export function buildQuery(pairs) {
  if (pairs.length === 0) return '';
  return '?' + pairs.map(([k, v]) => 
    `${encodeComponent(k)}=${encodeComponent(v)}`
  ).join('&');
}
Enter fullscreen mode Exit fullscreen mode

Pair format means:

  • Order is preserved (some APIs are order-sensitive)
  • Duplicate keys are supported
  • Empty values stay empty (?foo= is different from ?foo)
  • Adding or removing a specific occurrence is straightforward

The add/update/remove operations

export function addParam(pairs, key, value) {
  return [...pairs, [key, value]];
}

export function removeParam(pairs, index) {
  return pairs.filter((_, i) => i !== index);
}

export function updateParam(pairs, index, key, value) {
  return pairs.map((pair, i) => i === index ? [key, value] : pair);
}
Enter fullscreen mode Exit fullscreen mode

All immutable. Each UI action produces a new pairs array, which gets passed back into buildQuery to regenerate the search string, which combines with the other components via buildURL.

Port 443 gotcha

While writing tests, I discovered: the native URL API silently strips default ports. new URL('https://example.com:443/') gives you port: '' because 443 is the default for HTTPS. Same for 80 on HTTP, 21 on FTP, 22 on SSH.

// Tests use port 9000 instead of 443 to avoid this behavior
const parsed = parseURL('https://example.com:9000/path');
assert.strictEqual(parsed.port, '9000');
Enter fullscreen mode Exit fullscreen mode

If you need to preserve default ports literally, you'd have to detect them and carry them separately — the URL API doesn't let you.

IPv6 host handling

Another gotcha: URL.hostname for an IPv6 URL returns the address without brackets, but you need brackets when rebuilding the URL:

Input:  https://[::1]:8080/
hostname: "[::1]"  (actually returns with brackets in some implementations)
Enter fullscreen mode Exit fullscreen mode

The implementation varies by browser/Node version. The safe check is hostname.includes('::1') rather than exact match.

Encode vs encodeURIComponent

JavaScript has three URL-ish encoding functions:

  • encodeURI() — escapes only characters that would break URL structure (?, #, space). Preserves :, /, @, etc.
  • encodeURIComponent() — escapes everything that isn't A-Za-z0-9-_.!~*'()
  • escape() — deprecated, don't use

For URL components (query values, path segments), use encodeURIComponent. For whole URLs, use encodeURI (but it doesn't re-encode already-encoded chars, so round-tripping can be lossy).

Series

This is entry #92 in my 100+ public portfolio series.

Top comments (0)