Anatomy of a URL: every part explained, with query strings
· 7 min read
URLs are so familiar that we rarely look at them closely, until a redirect goes to the wrong page, a query parameter vanishes, or a link like https://google.com@evil.example/ sends someone to a phishing site. Each of those comes down to one of the seven parts of a URL behaving in a way most people don’t expect.
This article goes through a URL part by part, following the WHATWG URL Standard that browsers and Node.js implement. To take apart your own URLs, paste them into the URL parser, which splits a URL into its parts and lets you edit query parameters as a table.
The full picture
https://user:pass@example.com:8080/p/a/t/h?query=string&tag=a&tag=b#hash
scheme https
userinfo user:pass
host example.com
port 8080
path /p/a/t/h
query query=string&tag=a&tag=b
fragment hash
The browser’s URL class gives you each part:
const u = new URL('https://user:pass@example.com:8080/p/a/t/h?query=string&tag=a&tag=b#hash');
u.protocol; // "https:"
u.username; // "user"
u.password; // "pass"
u.hostname; // "example.com"
u.port; // "8080"
u.pathname; // "/p/a/t/h"
u.search; // "?query=string&tag=a&tag=b"
u.hash; // "#hash"
u.origin; // "https://example.com:8080"
The same breakdown appears in the URL parser as a table.
Scheme
The scheme (https, http, mailto, ftp, data, …) says how to interpret the rest. It’s case-insensitive, and parsers normalise it to lowercase: HTTPS://EXAMPLE.com/Path becomes https://example.com/Path. Note that the path kept its capital P. Paths are case-sensitive, and whether /Path and /path are the same page depends on the server.
Userinfo: credentials in the URL
user:pass@ before the host is legal syntax and a bad idea. Credentials end up in browser history, server logs and Referer headers, and modern browsers either strip them or show a warning.
Userinfo is also a phishing trick. In https://google.com@evil.example/login, the host is evil.example, and google.com is just a username. People read the start of the URL and trust it. The host is whatever comes after the last @ and before the next /, ? or #.
Host
The host is a domain name or an IP address. Domain names are case-insensitive. Non-ASCII domains are converted to Punycode for DNS, so https://пример.рф/ becomes https://xn--e1afmkfd.xn--p1ai/ in the parsed URL. That conversion is also the basis of homograph attacks, where a Cyrillic а imitates a Latin a. Browsers show Punycode instead of the Unicode name when a domain mixes scripts suspiciously.
IPv6 addresses go in brackets: http://[::1]:3000/.
Port
The port is optional. When it’s the default for the scheme (443 for HTTPS, 80 for HTTP), parsers drop it: http://example.com:80/ is normalised to http://example.com/, and url.port is an empty string. That’s worth remembering when you compare URLs or build cache keys.
The scheme, host and port together form the origin, which is what browsers use for same-origin policy, CORS and cookies scoped to a site.
Path
The path is a sequence of segments separated by /. Parsers resolve . and .. segments: /a/./b/../c becomes /a/c.
The trailing slash matters
/docs and /docs/ are different URLs. Servers often redirect one to the other, but relative links resolve differently depending on which one the page is at:
new URL('c', 'https://example.com/a/b').href; // "https://example.com/a/c"
new URL('c', 'https://example.com/a/b/').href; // "https://example.com/a/b/c"
new URL('/c', 'https://example.com/a/b/').href; // "https://example.com/c"
A relative link that works on /docs/ breaks on /docs. Pick one form for your site, redirect the other to it, and use it in your canonical URLs.
Query string
Everything between ? and # is the query. The URL spec treats it as one opaque string. The name=value&name=value convention comes from HTML forms (application/x-www-form-urlencoded), and URLSearchParams implements that convention.
Repeated parameters
A parameter can appear more than once, and get returns only the first value:
const p = new URLSearchParams('a=1&b=2&a=3');
p.get('a'); // "1"
p.getAll('a'); // ["1", "3"]
Servers don’t agree on what a=1&a=3 means. PHP and Rails keep the last value unless you write a[]=, Express (with its default parser) returns an array, and many frameworks keep the first value. That inconsistency has caused real bugs and security issues (“HTTP parameter pollution”), where a proxy validates one value and the backend uses another. If your API accepts lists, document the format and reject ambiguous input.
Order and caching
Parameter order doesn’t change the meaning for most servers, but it does change the URL string. ?a=1&b=2 and ?b=2&a=1 are two different cache keys for a CDN unless it normalises them. If you generate URLs, sort parameters or build them the same way every time.
Encoding
Values must be percent-encoded, and in the query, a space may be written as +. The rules and the difference between encodeURI and encodeURIComponent are covered in URL encoding explained, and you can try them in the URL encoder. The URL parser decodes values for display and re-encodes them when you edit the table, so a value with & or = stays intact.
Fragment
Everything after # is the fragment. The browser never sends it to the server: it’s handled entirely on the client, for jumping to an element with a matching id, and by single-page apps for routing (/#/settings). That makes it a place where tokens sometimes end up. The OAuth implicit flow returned access tokens in the fragment, precisely so they wouldn’t reach server logs. The flow is now discouraged in favour of the authorization code flow with PKCE.
Because the fragment never reaches the server, server-side redirects and logs can’t see it.
Absolute and relative URLs
A relative URL is resolved against a base URL:
| Relative | Base | Result |
|---|---|---|
c |
https://example.com/a/b |
https://example.com/a/c |
c |
https://example.com/a/b/ |
https://example.com/a/b/c |
/c |
https://example.com/a/b/ |
https://example.com/c |
?x=2 |
https://example.com/a/b?x=1 |
https://example.com/a/b?x=2 |
//cdn.example.com/x.js |
https://example.com/ |
https://cdn.example.com/x.js |
The last row is a protocol-relative URL. It was popular when sites served both HTTP and HTTPS; today you can simply write https://.
A string without a scheme, such as example.com/x, isn’t an absolute URL at all. URL.canParse('example.com/x') returns false, which is a quick way to validate user input before you store it as a link.
How long can a URL be?
The URL spec sets no limit. In practice, browsers handle URLs of hundreds of kilobytes, but servers, proxies and CDNs often reject request lines of more than 8 KB (a common default in Apache and nginx), and some analytics and email tools truncate much earlier. Keeping URLs under about 2,000 characters avoids trouble. For more data, use a POST body.
Checklist for building URLs in code
- Build URLs with the
URLclass andsearchParams, not string concatenation. - Encode each value, never the whole URL.
- Decide on trailing slashes and redirect the other form.
- Keep credentials and secrets out of URLs; they end up in logs.
- When a URL behaves strangely, look at its parts one at a time.
For the last point, paste the URL into the URL parser. You’ll see exactly what the browser sees, and you can add, edit or remove query parameters and copy the rebuilt URL.