A short history of URLs and URIs
When Tim Berners-Lee designed the web in 1991, he needed a single, universal way to point at any document anywhere. That idea became the URL. It was formalized in 1994 as RFC 1738, refined into the broader URI framework by RFC 3986 in 2005, and is now maintained as a living document by the WHATWG URL Standard — the version browsers actually implement today.
The terminology can be confusing. A URI (Uniform Resource Identifier) is the umbrella term for any identifier. A URL (Uniform Resource Locator) is a URI that also says where to find the thing and how to reach it. A URN (Uniform Resource Name) names something without locating it. In practice “URL” is what everyone means, and it is what the URL Parser is built to dissect.
The full skeleton
Every URL follows the same template, with each component marked off by a reserved delimiter:
scheme://userinfo@host:port/path?query#fragment
Not every part is always present — most URLs you click are just scheme://host/path — but the slots are always in this order, and the delimiters never move. Learn the delimiters and you can read any URL at a glance.
Scheme
The scheme is the protocol prefix: https, http, mailto, ftp, file, ws, wss. It ends at the first colon and tells the client how to handle the rest of the URL. Schemes are case-insensitive and normalized to lowercase. A handful of “special” schemes (http, https, ws, wss, ftp, file) carry extra rules: they require the // that introduces an authority, and they each have a default port.
Authority: userinfo, host, and port
The authority is introduced by // and answers “who serves this resource?”
Userinfo — an optional username:password@ prefix. Embedding credentials in a URL is now discouraged, and browsers strip or warn on them, but the syntax remains valid.
Host — the domain name or IP literal. IPv6 addresses are wrapped in brackets: https://[2001:db8::1]/. The host value includes a port if present; hostname never does.
Port — the TCP port. Crucially, ports have scheme defaults, so they are usually omitted.
Ports and their defaults
Every special scheme defines a default port:
| Scheme | Default port |
|---|---|
| http | 80 |
| https | 443 |
| ws | 80 |
| wss | 443 |
| ftp | 21 |
When a URL uses its default port, the WHATWG URL parser reports the port as an empty string rather than the number — the default is implied. So https://example.com and https://example.com:443 are equivalent, and both report no explicit port. Only a non-default port like :8080 appears in the parsed output. This is intentional normalization, not a bug, and it surprises nearly everyone the first time.
Path and segments
The path begins at the first / after the host and identifies the specific resource. Splitting it on / yields ordered segments that map onto REST routes, file directories, and framework routers. A trailing slash produces a distinct path, and servers may treat /docs and /docs/ differently — a frequent cause of redirects and 404s. Dot-segments (. and ..) are resolved away during parsing, so /a/b/../c normalizes to /a/c.
Internationalized domain names and punycode
The global DNS only understands ASCII. So a domain like café.com or münchen.de cannot be sent over the wire as-is. The browser converts the Unicode hostname into punycode, an ASCII-compatible encoding prefixed with xn--. café.com becomes xn--caf-dma.com. You will see the human-readable form in the address bar but the punycode form in DNS lookups and TLS certificates. A good parser shows you the normalized ASCII host so you know exactly what will be resolved — and so you can spot lookalike “homograph” domains used in phishing.
Query parameters and their conventions
The query string starts at ? and ends at #. By convention it is a list of key=value pairs joined by &:
?page=2&sort=date&q=hello%20world
A few conventions worth knowing:
- Repeated keys are valid and meaningful (
?id=1&id=2). UseURLSearchParams.getAll()to retrieve every value. - Empty values (
?flag=) and bare keys (?flag) are both legal and parse slightly differently across stacks. - Encoding is mandatory for spaces,
&,=, and any non-ASCII character inside a value. +for spaces is a form-encoding (application/x-www-form-urlencoded) convention, not part of the generic URL rules —URLSearchParamshonors it, raw decoders may not.
The query string is opaque to the URL standard itself; structure is imposed by URLSearchParams, which is what the URL Parser uses to render a clean parameter table.
The fragment and how SPAs use it
The fragment is everything after #. It has two defining properties: it is never transmitted to the server, and it does not trigger a page reload when changed. Originally it just pointed at a named anchor so the browser could scroll to a heading.
Single-page applications repurposed it for client-side routing. Because changing #/dashboard to #/settings updates the URL without a server round-trip, early SPA routers built their entire navigation on the fragment (hash-based routing). Modern apps prefer the History API — history.pushState() and popstate — which lets them update the full path (/settings) without a reload while keeping clean, shareable URLs. The fragment is still widely used for in-page anchors, OAuth implicit-flow tokens, and deep-linking into client state. Either way, the server never sees it.
Putting it together
A URL only looks like a flat string. It is really seven labeled slots separated by fixed delimiters, each with its own rules — default ports, punycode hosts, encoded query values, and a fragment that never leaves the browser. Once you can name every part, debugging links, building API calls, and reasoning about routing all get dramatically easier.
For a hands-on, scenario-driven walkthrough with debugging tips, read How to Parse a URL. And whenever you want to see a real URL split into all of these components instantly and privately in your browser, reach for the URL Parser.