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…