iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

URLs

URL validation is a common task, but URL regex is notoriously tricky. The pragmatic answer is the same as for emails: use the URL constructor for parsing, then a regex (or allowlist) for the structural constraints you actually care about — scheme, host, ports, paths.

URL parser + targeted regex patterns

EXAMPLE
// 1) Use the URL constructor first — it's a real parser
function parseUrl(input) {
    try {
        return new URL(input);
    } catch {
        return null;
    }
}

parseUrl('https://example.com/path?q=1#section')?.pathname;   // '/path'
parseUrl('not a url');                                           // null

// The URL constructor handles:
//   • Percent-encoding
//   • IDN domains (Punycode encoding)
//   • IPv6 bracket notation
//   • Default ports
//   • Relative URLs (with a base URL)

// 2) Simple shape regex (when you can't use a parser, e.g. lightweight templates)
const URL_RE = /^(?:(?:https?|ftp):\/\/)?(?:[A-Za-z0-9-]+\.)+[A-Za-z]{2,}(?::\d+)?(?:\/[^\s]*)?$/i;

URL_RE.test('https://example.com');                  // true
URL_RE.test('example.com/path');                      // true
URL_RE.test('https://example.com:8080/path?q=1');    // true
URL_RE.test('javascript:alert(1)');                   // false (no scheme allowed unless http/https/ftp)

// 3) Tighter HTTPS-only validation
const HTTPS_RE = /^https:\/\/(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}(?::\d+)?(?:\/[^\s]*)?$/i;

HTTPS_RE.test('https://example.com');                 // true
HTTPS_RE.test('http://example.com');                   // false
HTTPS_RE.test('https://localhost');                    // false (no TLD)

// 4) Allow localhost / IPs for dev environments
const URL_DEV_RE = /^(https?):\/\/(localhost|(?:\d{1,3}\.){3}\d{1,3}|(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,})(?::\d+)?(?:\/[^\s]*)?$/i;

URL_DEV_RE.test('http://localhost:3000');             // true
URL_DEV_RE.test('http://192.168.1.1');                // true
URL_DEV_RE.test('https://example.com');                // true

// 5) Domain validation alone
const DOMAIN_RE = /^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,}$/;

DOMAIN_RE.test('example.com');                          // true
DOMAIN_RE.test('sub.example.com');                       // true
DOMAIN_RE.test('-bad.com');                              // false (label can't start with hyphen)
DOMAIN_RE.test('a.b');                                    // false (TLD must be >= 2 chars)

// 6) Path / query string extraction
const PATH_QUERY_RE = /^([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/;
const [, path, query, hash] = 'https://example.com/foo/bar?q=1#top'.replace(/^https?:\/\/[^/]+/, '').match(PATH_QUERY_RE) ?? [];
// path: '/foo/bar', query: 'q=1', hash: 'top'

// 7) Slug regex — URL-safe identifiers
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;

SLUG_RE.test('hello-world');                              // true
SLUG_RE.test('Hello-World');                               // false (uppercase)
SLUG_RE.test('hello--world');                              // false (double hyphen)
SLUG_RE.test('-hello');                                    // false (leading hyphen)
SLUG_RE.test('hello-');                                    // false (trailing hyphen)

function slugify(s) {
    return s.toLowerCase()
        .replace(/[^a-z0-9]+/g, '-')
        .replace(/(^-|-$)/g, '');
}

// 8) Real-world: classify URLs by scheme — defense in depth for href validation
const ALLOWED = new Set(['http:', 'https:', 'mailto:', 'tel:']);

function safeHref(input) {
    const u = parseUrl(input);
    if (!u) return null;
    if (!ALLOWED.has(u.protocol)) return null;
    return u.toString();
}

safeHref('https://example.com');     // valid
safeHref('javascript:alert(1)');     // null — rejected
safeHref('data:text/html,...');      // null — rejected

// 9) Use cases where regex IS the right tool
// • Lightweight client-side validation (immediate feedback)
// • Routing rules (Express path matching)
// • Markdown URL extraction (find every URL in text)
// • Log parsing (extract URLs from access logs)
// • CSP source list matching

// 10) Use cases where you should NOT use a regex
// • Production security validation — use URL parser + allowlist
// • RFC compliance — the URL standard is too complex for a regex
// • Internationalised domains — use the URL constructor (handles Punycode)
// • Detecting all URLs in arbitrary text — use a battle-tested library (linkify-it)

// 11) Common patterns
// IPv4
const IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\d|1?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|1?\d?\d)$/;

// IPv6 (simplified — full spec is huge)
const IPV6_RE = /^(?:[0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}$|^::(?:[0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}$/;

// URL with query parameters extraction
const QUERY_PARAMS_RE = /[?&]([^=&]+)=([^&]*)/g;
const u = 'https://example.com/?a=1&b=2&c=hello+world';
const params = [...u.matchAll(QUERY_PARAMS_RE)].map(([, k, v]) => [k, decodeURIComponent(v)]);
// [['a','1'], ['b','2'], ['c','hello world']]

// 12) Extracting URLs from text
const URLS_IN_TEXT_RE = /https?:\/\/[^\s)\]'"<>]+/g;
const text = 'Visit https://example.com or https://test.com/path for more.';
text.match(URLS_IN_TEXT_RE);
// ['https://example.com', 'https://test.com/path']

// 13) Edge cases worth testing
// • Trailing slash:    https://example.com/ vs https://example.com
// • Default port:      https://example.com:443 vs https://example.com
// • Unicode domain:    https://例え.jp
// • Long URL:          paths up to 2000 chars (browser limit)
// • Embedded auth:     https://user:pass@example.com (deprecated; many parsers reject)
// • IPv6 with port:    https://[::1]:8080
// • File:              file:///etc/passwd
// • UNC:               \\\\\\\\server\\share — not really a URL

// 14) Performance
// • Compile regex once; reuse it
// • Avoid catastrophic backtracking — keep quantifiers bounded
// • For matching against many URLs (allowlists), use a Set or trie, not a giant alternation

// 15) Common bugs
// • Greedy matching past expected boundaries — anchor with ^ and $
// • Forgetting case-insensitive flag for schemes — HTTP vs http
// • Naive (.+) for paths matches spaces; restrict to [^\\s] or specific chars
// • Allowing javascript: through a regex that didn't anchor scheme — XSS
// • Storing URLs without normalisation — duplicates differ only in trailing slash / case
// • Using a regex for URL parsing when you have a URL parser available — almost always the wrong move
// • Forgetting that '+' in a query string means space (decodeURIComponent does NOT decode it; use URLSearchParams)

Why it matters

For real URL validation, use new URL() and an allowlist of schemes — regex misses too many corner cases (IDN, IPv6, edge schemes) to be a security boundary. Reserve regex for cheap shape checks, slug rules, query parameter extraction, or finding URLs in free-form text, and always pair regex hits with the parser for the final say.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
/^(https?:\/\/)?([\w-]+\.)+[a-z]{2,}(\/[^\s]*)?$/i
// Better: use the URL constructor where the language has one.
try { new URL(s); } catch { /* not a URL */ }
Try it Yourself »

Discussion

Loading…