How to Parse a URL: A Field Guide to Reading Every Part of a Link
Learn to parse a URL into scheme, host, port, path, query and fragment, read query parameters, and debug tracking links and encoding with a URL parser.
How to Parse a URL: A Field Guide to Reading Every Part of a Link
A long URL pasted into a chat window looks like one undifferentiated string. But it is not one thing. It is a stack of labeled slots, and once you can see the slots, a 200-character link stops being intimidating and becomes a small structured record you can read at a glance. This post walks through every part of a URL, how to pull values out of the query string, and the handful of mistakes that cost people an afternoon of debugging.
The shape of a URL
Every absolute URL follows the same template:
scheme://host:port/path?query#fragment
Take a concrete one:
https://shop.example.com:8443/cart/items?id=42&id=99&promo=spring+sale#summary
Reading left to right, that splits into:
- scheme:
https— the protocol the client uses to reach the server. - host:
shop.example.com— the machine being addressed. The browser also splits this into labels (shop,example,com). - port:
8443— where the server listens. If you omit it, the scheme's default applies (443 for https, 80 for http). - path:
/cart/items— the resource on that host, split into segments (cart,items). - query:
id=42&id=99&promo=spring+sale— the part after the?. - fragment:
summary— the part after the#.
Two of these slots are worth dwelling on, because they behave differently from the rest.
The query string is a list, not a dictionary
The query is everything after the ?. It is made of key=value pairs joined by &. That is the whole grammar. In the example above, the query carries three pairs: id=42, id=99, and promo=spring+sale.
The trap is treating the query like a dictionary where each key maps to one value. It does not. A key can repeat. Here id appears twice, holding both 42 and 99. If you write new URLSearchParams(query).get('id') in JavaScript, you get back only 42 — the first occurrence — and the second value vanishes silently. To see them all you need getAll('id'), which returns ['42', '99']. A good URL parser lists every occurrence on its own row instead of collapsing them, so a duplicate that changes behavior never hides from you.
The second detail: values are percent-encoded. promo=spring+sale is not literally spring+sale. In a query string, + decodes to a space, and sequences like %20 or %E4%B8%AD decode to a space or a multibyte character. So promo actually reads spring sale. Comparing the raw encoded form against readable text is how people end up debugging a campaign that "doesn't match" when it matches fine once decoded.
The fragment is client-side only
The fragment is everything after the #. Two things make it special. First, the browser never sends it to the server — it stays in the client. Request a page with #summary on the end and the server has no idea the fragment exists; only your JavaScript and the browser's scroll-to-anchor behavior see it. Second, the URL engine does not decode or split it. A fragment can be a plain anchor (#summary), a query-like routing string used by a single-page app (#/dashboard?tab=2), or even a token. So it is handed back verbatim, and it is your job to interpret it.
A worked example
Let me run the full link through, the way the tool does.
Input:
https://shop.example.com:8443/cart/items?id=42&id=99&promo=spring+sale#summary
Parsed parts:
| Part | Value | |---|---| | scheme | https | | host | shop.example.com:8443 | | hostname | shop.example.com | | port | 8443 | | path | /cart/items | | query | id=42&id=99&promo=spring+sale | | fragment | #summary |
Query parameter table:
| Key | Decoded value | |---|---| | id | 42 | | id | 99 | | promo | spring sale |
Notice three things that a casual read would miss. The host field carries the port (shop.example.com:8443) while the hostname does not (shop.example.com) — mixing those two up when matching an origin or a CORS rule produces phantom mismatches. The id key appears twice, so .get('id') would have hidden the 99. And promo decodes to a real space, not a +.
Debugging tracking links and redirects
This is where I reach for a parser most often. A teammate forwards a marketing link, reports that the wrong campaign got attributed, and the link is a single unbreakable line. Pasting it in lays out utm_source, utm_medium, and utm_campaign each on its own row, decoded. More than once I have found that a second stray utm_source was overriding the first, or that utm_campaign held a raw space where an encoded one belonged. Reading the table takes two seconds; squinting at the raw string takes ten minutes and you still miss the duplicate.
OAuth and SSO callbacks are the same story at higher stakes. Those URLs come back stuffed with code, state, scope, and redirect_uri, frequently double-encoded. Splitting them out lets you confirm the state matches what you sent and that redirect_uri points where you expect, instead of chasing a login loop blind.
When you need to encode, not just read
Parsing is the read direction. The write direction — building a URL with a value that contains spaces, ampersands, or non-Latin characters — needs encoding, and getting it wrong is exactly what produces the broken links you later have to parse. If a value of yours holds anything beyond plain letters and digits, run it through a URL encoder before you stitch it into a query string. Encode the value, not the whole URL, or you will turn your ? and & separators into literal characters and break the structure.
One note on absolute versus relative
A bare string like example.com/path is not an absolute URL — it is a relative reference, and the URL engine rejects it without a base to resolve against. There is no protocol to tell it whether you meant https://, ftp://, or something else. Rather than silently guessing, an honest parser asks you to add a scheme. Prefix it with https:// and it resolves immediately. This is not pedantry: the same rejection is why a fetch call or a server router would refuse the bare string too, so seeing the parser refuse it tells you something real about the input.
Once you internalize the scheme://host:port/path?query#fragment template and remember that the query is a repeatable list of percent-encoded pairs while the fragment never leaves the client, most URL confusion evaporates. The string was always structured. You just needed to see the slots.
Made by Toolora · Updated 2026-06-13