A URL looks like one long string, but it is really a stack of precisely defined fields, each read by a different actor: the browser, the DNS resolver, the load balancer, your router, and finally your handler function. Knowing which part does what is the difference between guessing at an API design and defending one.
This post takes a single URL apart piece by piece, then spends real time on the question that comes up in every API review and half of all backend interviews: should this value be a path parameter or a query parameter?
1. The full anatomy of a URL
Here is a URL with essentially every optional part present:
scheme subdomain domain TLD port path query fragment
│ │ │ │ │ │ │ │
https :// blog . archtin . com : 443 /posts/url-parts ?ref=newsletter&page=2 #anatomy
└────────────── authority ──────────────┘Formally (RFC 3986) the shape is scheme://userinfo@host:port/path?query#fragment. Only the scheme and the host are mandatory for an HTTP URL — everything else has a default or can be omitted.
| Part | Example | Who reads it | Sent to the server? |
|---|---|---|---|
| Scheme | https | Browser | No (implied) |
| Subdomain | blog | DNS | Yes, in Host header |
| Domain + TLD | archtin.com | DNS / registry | Yes, in Host header |
| Port | 443 | TCP stack | No |
| Path | /posts/url-parts | Your router | Yes |
| Query | ?ref=newsletter | Your handler | Yes |
| Fragment | #anatomy | Browser only | No — never leaves the client |
# are invisible to your server logs, and an access token in a fragment (the old OAuth implicit flow) stays client-side by design.2. Scheme (protocol)
The scheme tells the browser how to talk, not where. It ends at the :; the // that follows means "an authority component comes next."
https— HTTP over TLS. Default port 443. The only acceptable scheme for a production site; browsers gate geolocation, service workers, camera and HTTP/2 behind it.http— plaintext, port 80. Keep it only as a 301 redirect to https.mailto:,tel:,sms:— no authority at all, hence no//.mailto:hi@archtin.comis a complete URL.data:andblob:— inline or in-memory content, common for generated images and downloads.- Custom app schemes like
myapp://order/42deep-link into mobile apps.
A URL beginning with // and no scheme is protocol-relative; it inherits the current page's scheme. It is a legacy trick — just write https.
3. Authority: host, subdomain, TLD, port
The authority is everything between // and the next /. Its core is the host, which reads right-to-left as a hierarchy.
Subdomain
A label in front of the registered domain. www is a convention, not a requirement. Subdomains are separate origins for the browser's same-origin policy, so app.archtin.com cannot read archtin.com's localStorage — a real architectural decision, not cosmetics. Teams often split api., cdn., docs. and status. so each can point at different infrastructure and carry different cookies and cache rules.
Second-level domain
The name you actually registered. Combined with the TLD it forms the registrable domain — the unit that DNS, certificates and cookies care about.
Top-level domain
.com, .org, .dev, .in. Some TLDs carry behaviour: .dev and .app are on the HSTS preload list, so browsers refuse plaintext HTTP on them entirely. Country TLDs can also hint geographic targeting to search engines.
Port
Which process on the host answers. Omitted in practice because 80 and 443 are the defaults for http and https; you see it mostly in local development (localhost:8080). The port is used to open the socket and is never sent inside the request itself.
- 1Parse URLBrowser splits scheme, host, path, query, fragment
- 2DNS lookupblog.archtin.com → 104.21.x.x
- 3TCP + TLSConnect on the port (443 for https)
- 4HTTP requestGET /posts/url-parts?ref=newsletter
- 5Render + scrollFragment #anatomy applied client-side only
4. Path
The path starts at the first / after the authority and names a resource on that host. Originally it mapped to a file on disk (/about/index.html); today it is almost always a routing pattern matched by your framework.
- Segments are separated by
/and are ordered and hierarchical —/users/42/ordersreads as "orders belonging to user 42." - Paths are case-sensitive on the server side per spec (unlike the host, which is not).
/Aboutand/aboutmay be two pages — pick lowercase and redirect the rest. - A trailing slash is technically a different URL. Pick one form, 301 the other, and set a canonical tag.
- Dynamic segments in frameworks are written
/users/:id,/users/$idor/users/[id]depending on the router; they all produce the same URL.
/blog/parts-of-a-url-explained ← slug, human readable /users/42/orders/7781 ← nested ownership /docs/v2/authentication ← versioned section /files/report.pdf ← extension only when the format is fixed /p?id=3821 ✗ opaque /Blog/Parts-Of-A-URL/ ✗ mixed case + trailing slash
5. Query string
Everything after the first ? up to a #. It is a flat list of key=value pairs joined by &, and unlike the path it is unordered and freely repeatable.
?status=shipped&page=2&sort=-created_at&tag=urgent&tag=late
- Repeated keys are legal.
tag=urgent&tag=lateis how you send an array; most frameworks expose it as a list (some also accepttag[]=). - Empty values (
?q=) and bare flags (?debug) are both valid and mean different things to different parsers — be explicit. - Encoding: spaces become
%20or+, and&,=,#,?inside a value must be percent-encoded or they will be parsed as delimiters. - Order affects caching.
?a=1&b=2and?b=2&a=1are semantically identical but are different cache keys for most CDNs. Sort your params when you generate links. - Length limits are practical, not spec'd — stay under ~2,000 characters for the whole URL to be safe across browsers, proxies and log systems.
6. Fragment
Everything after #. It points within the returned document and is resolved entirely by the client: the browser scrolls to the element whose id matches, or to a named anchor. Changing it does not trigger a page load, which is why old SPAs used #/route hash routing.
#pricing— scroll to the element withid="pricing".#:~:text=path%20parameter— a text fragment; Chrome highlights and scrolls to the matching text without any id existing.- Never carries to the server, so never useful for server-side analytics or auth.
7. Userinfo, encoding & relative URLs
Userinfo
https://user:pass@example.com is valid syntax and browsers largely refuse it now — it was a classic phishing vector (https://paypal.com@evil.tld goes to evil.tld). Treat it as legacy; send credentials in headers.
Percent-encoding
URLs are ASCII. Anything else — spaces, emoji, Devanagari, or a reserved character used literally — is percent-encoded as UTF-8 bytes. Two functions, two jobs:
encodeURIComponent("a/b?c=d") // "a%2Fb%3Fc%3Dd" ← for a single value
encodeURI("https://x.com/a b") // "https://x.com/a%20b" ← for a whole URL
// Internationalised hostnames use punycode, not percent-encoding:
// "https://архтин.рф" → "https://xn--80atbfjhi.xn--p1ai"Absolute vs relative
| Written as | Resolved from /blog/a/b | Called |
|---|---|---|
| https://x.com/c | https://x.com/c | Absolute |
| /c | https://site.com/c | Root-relative |
| c | https://site.com/blog/a/c | Relative |
| ../c | https://site.com/blog/c | Parent-relative |
| //cdn.x.com/c | current scheme + cdn.x.com/c | Protocol-relative |
8. Path parameters vs query parameters
Both put data in the URL, both are readable by the server, both are cacheable. The difference is semantic: a path parameter says which thing, a query parameter says how you want it.
| Path parameter | Query parameter | |
|---|---|---|
| Syntax | /orders/7781 | /orders?id=7781 |
| Role | Identifies a resource | Filters / modifies a collection |
| Required? | Yes — route won't match without it | Optional by nature |
| Order | Significant and hierarchical | Irrelevant |
| Multiple values | Awkward | Natural (repeat the key) |
| Empty value | Not possible | Fine (?q=) |
| Caching | Clean, one URL per resource | Fragmented by param order/combos |
| SEO | Preferred for indexable pages | Often ignored or canonicalised away |
| Typical use | IDs, slugs, versions, nested ownership | Search, sort, page, filter, utm |
The decision list
Worked examples
GET /users/42 → one user (path: identity) GET /users/42/orders → that user's orders (path: ownership) GET /users/42/orders?status=open → filtered subset (query: refinement) GET /orders?user_id=42 → also valid: a flat collection filtered by owner GET /search?q=redis&page=3&sort=new → no resource identity at all; all query GET /orders/status/open ✗ "open" isn't an order — it's a filter GET /users?id=42 ✗ singular resource hidden behind a filter GET /report?token=secret ✗ secrets in URLs land in logs and Referer headers
The grey areas
- API versions.
/v2/users(path) is the common choice — easy to route and cache. Purists prefer a header;?version=2is the worst of both. - Locale.
/en/pricingbeats?lang=enfor SEO because each language gets its own indexable URL. - Composite keys.
/teams/9/members/42reads better than/members?team=9&user=42when the child cannot exist without the parent. - Big filter payloads. When a query string would exceed ~2 KB, switch to
POST /searchwith a JSON body and return a short shareable id. - Matrix parameters (
/maps/x=1;y=2) exist in the spec and are almost never used. Don't reintroduce them.
9. Design rules & anti-patterns
- Lowercase, hyphen-separated, no file extensions for HTML pages.
- Plural collection nouns, singular ids:
/articles/12/comments/3. - No verbs in paths — the HTTP method is the verb.
DELETE /posts/3. - Keep URLs stable. If one must change, 301 the old one forever; links and rankings are attached to the string.
- Never put tokens, passwords, emails or session ids in a URL. They leak into server logs, browser history, CDN caches and the
Refererheader of every outbound link. - Strip or canonicalise tracking params (
utm_*,fbclid) before they become duplicate indexed pages. - Sort query params when generating links so CDNs get one cache entry, not six.
10. Parsing URLs in code
Never parse URLs with string splitting or regex. Every runtime ships the WHATWG URL class, which handles encoding, defaults and relative resolution for you.
const u = new URL("https://blog.archtin.com:443/posts/url-parts?ref=nl&tag=a&tag=b#anatomy");
u.protocol // "https:" (note the colon)
u.hostname // "blog.archtin.com"
u.port // "" (empty — 443 is the default for https)
u.origin // "https://blog.archtin.com"
u.pathname // "/posts/url-parts"
u.search // "?ref=nl&tag=a&tag=b"
u.hash // "#anatomy"
u.searchParams.get("ref") // "nl"
u.searchParams.getAll("tag") // ["a", "b"]
u.searchParams.set("page", "2");
u.searchParams.sort(); // stable cache key
u.toString(); // rebuilt, correctly encoded
// Relative resolution — the second argument is the base
new URL("../pricing", "https://archtin.com/blog/a/b").href
// "https://archtin.com/blog/pricing"11. URLs, SEO & sharing
- Readability is a ranking-adjacent signal. Descriptive slugs get more clicks in search results and are shown verbatim in breadcrumbs.
- Depth. Keep important pages within two or three segments of the root.
- Duplicates. Query-parameter variants of the same content need a
rel="canonical"pointing at the clean path. - Faceted navigation (filters as query params) can generate thousands of near-identical URLs — canonicalise or block them in
robots.txt. - Share previews resolve against the full absolute URL, so keep
og:urlcanonical and absolute.
12. Cheat sheet
https://blog.archtin.com:443/posts/url-parts?ref=nl#anatomy └─┬─┘ └──┬──────────┘ └┬┘└──────┬──────┘└───┬───┘└───┬──┘ scheme host port path query fragment scheme how to connect https, mailto, tel host where to connect subdomain . domain . TLD port which process 80 http, 443 https path which resource ordered, hierarchical, required query how you want it unordered, optional, repeatable fragment where in the document client-only, never sent path parameter → identity /users/42/orders/7781 query parameter → refinement /orders?status=open&page=2&sort=-date
Get these boundaries right and your URLs document your system for free: someone can read one line of a log and know exactly which resource was touched and how it was filtered.