All articles
WebHTTPAPI Design14 min read

Every Part of a URL, Explained — Scheme, Host, Path, Query & Fragment

A visual breakdown of every component of an HTTP URL — scheme, subdomain, domain, TLD, port, path, query string, fragment and more — plus a deep section on path parameters vs query parameters and when to use each.

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:

httpsscheme
://
blogsubdomain
.
archtindomain
.comTLD
:443port
/posts/url-partspath
?ref=newsletter&page=2query string
#anatomyfragment
Every component of an HTTP URL, colour-coded.
  scheme    subdomain  domain  TLD  port          path              query                 fragment
   │            │        │      │    │              │                  │                      │
https :// blog . archtin . com : 443 /posts/url-parts ?ref=newsletter&page=2 #anatomy
      └────────────── authority ──────────────┘
The same URL written as a grammar.

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.

PartExampleWho reads itSent to the server?
SchemehttpsBrowserNo (implied)
SubdomainblogDNSYes, in Host header
Domain + TLDarchtin.comDNS / registryYes, in Host header
Port443TCP stackNo
Path/posts/url-partsYour routerYes
Query?ref=newsletterYour handlerYes
Fragment#anatomyBrowser onlyNo — never leaves the client
The one people get wrong
The fragment is never transmitted. Analytics that live in # 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.com is a complete URL.
  • data: and blob: — inline or in-memory content, common for generated images and downloads.
  • Custom app schemes like myapp://order/42 deep-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.

blog
subdomain
Which service / section on the host
archtin
second-level domain
The name you registered
com
top-level domain
Registry the name lives under
A hostname is a right-to-left hierarchy, not a flat string.

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.

  1. 1
    Parse URL
    Browser splits scheme, host, path, query, fragment
  2. 2
    DNS lookup
    blog.archtin.com → 104.21.x.x
  3. 3
    TCP + TLS
    Connect on the port (443 for https)
  4. 4
    HTTP request
    GET /posts/url-parts?ref=newsletter
  5. 5
    Render + scroll
    Fragment #anatomy applied client-side only
What each part of the URL is used for, in order, when you press Enter.

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/orders reads as "orders belonging to user 42."
  • Paths are case-sensitive on the server side per spec (unlike the host, which is not). /About and /about may 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/$id or /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
Path shapes that read well.

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=late is how you send an array; most frameworks expose it as a list (some also accept tag[]=).
  • Empty values (?q=) and bare flags (?debug) are both valid and mean different things to different parsers — be explicit.
  • Encoding: spaces become %20 or +, and &, =, #, ? inside a value must be percent-encoded or they will be parsed as delimiters.
  • Order affects caching. ?a=1&b=2 and ?b=2&a=1 are 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 with id="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 asResolved from /blog/a/bCalled
https://x.com/chttps://x.com/cAbsolute
/chttps://site.com/cRoot-relative
chttps://site.com/blog/a/cRelative
../chttps://site.com/blog/cParent-relative
//cdn.x.com/ccurrent scheme + cdn.x.com/cProtocol-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
/users/42/orders/7781
Identifies which resource. Hierarchical, required, part of the resource's identity. Change it and you are looking at a different thing.
Query parameter
/orders?status=shipped&page=2&sort=-date
Modifies how you see the collection. Optional, unordered, repeatable. Drop them all and the request is still valid.
Identity lives in the path. Presentation lives in the query.
Path parameterQuery parameter
Syntax/orders/7781/orders?id=7781
RoleIdentifies a resourceFilters / modifies a collection
Required?Yes — route won't match without itOptional by nature
OrderSignificant and hierarchicalIrrelevant
Multiple valuesAwkwardNatural (repeat the key)
Empty valueNot possibleFine (?q=)
CachingClean, one URL per resourceFragmented by param order/combos
SEOPreferred for indexable pagesOften ignored or canonicalised away
Typical useIDs, slugs, versions, nested ownershipSearch, sort, page, filter, utm

The decision list

Does it identify one specific resource?Path
Is it required for the request to make sense?Path
Does it filter, sort, paginate or search?Query
Is it optional or repeatable?Query
Is it a tracking / campaign tag (utm_*)?Query
Is it secret (token, password, PII)?Neither — use a header or body

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
Same domain, different intents.
The rule of thumb
If removing the value makes the request meaningless, it belongs in the path. If removing it just gives you a broader or unsorted result, it belongs in the query.

The grey areas

  • API versions. /v2/users (path) is the common choice — easy to route and cache. Purists prefer a header; ?version=2 is the worst of both.
  • Locale. /en/pricing beats ?lang=en for SEO because each language gets its own indexable URL.
  • Composite keys. /teams/9/members/42 reads better than /members?team=9&user=42 when the child cannot exist without the parent.
  • Big filter payloads. When a query string would exceed ~2 KB, switch to POST /search with 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 Referer header 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"
The URL and URLSearchParams APIs cover nearly every case.

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:url canonical 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.

Keep reading

Suggested next articles based on this one.

Design it, don't just read it.

Practise LLD and system design problems with structured rubrics and AI feedback.

Start practising free