Skip to content
Path Parameters & Query Strings
step 1/5

Reading — step 1 of 5

Read

~3 min readRouting & Responses

Path Parameters & Query Strings

/users/42/posts/7 — no server registers that path literally; there are a billion possible IDs. Real routing matches patterns (/users/:id/posts/:post_id) and extracts the variables. Add the query string (?page=2&sort=desc) and you have the two channels through which every web API receives its inputs. This lesson builds both extractors.

Pattern routes: match by segments

The registered pattern and incoming path both split on / into segments; matching walks them in lockstep:

pattern:  /users/:id/posts/:post_id     → [users, :id, posts, :post_id]
request:  /users/42/posts/7             → [users, 42,  posts, 7       ]

Rules per position: a literal segment must match exactly; a :param segment matches any single segment and captures it. Segment counts must be equal — /users/42 (2 segments) cannot match the 4-segment pattern; no partial credit. Result of a match: the handler plus a params map, {id: "42", post_id: "7"} — and note the values are strings. The router doesn't know :id is numeric; handlers validate and convert (/users/abc routes fine; the handler's "abc isn't an ID" → 404 or 400 is an application decision, cleanly outside the router).

Precedence: the ordering question every router answers

/users/new — literal — and /users/:id — pattern — can both match /users/new. Which wins? The rule almost every framework converged on: static beats dynamic; the most specific match wins, regardless of registration order. Otherwise adding a pattern route can silently steal an existing literal route — the kind of spooky breakage frameworks exist to prevent. Your implementation: check literal-segment matches before param captures at each position (or two-pass: exact routes first, then patterns). Your spec fixes the rule; the concept — specificity, not registration order — is the transferable part.

Query strings: the other input channel

Everything after ? is the query string, and it never participates in routing — strip it before path matching (/search?q=x routes as /search), parse it separately:

?q=redis&page=2&sort=            → {q: "redis", page: "2", sort: ""}

Grammar: &-separated pairs, each key=value. The corners that distinguish a parser from a split("&"):

  • Percent-decoding: q=hello%20worldhello world; + also means space in query strings (form-encoding legacy — decode both).
  • Empty values are present: sort= is the key sort with value "" — distinct from absent. Some grammars also allow bare keys (?debug) — spec-dependent.
  • Duplicate keys (?tag=a&tag=b): legal, meaningful (multi-select forms), policy required — last-wins or list, per your spec. (The same "have a policy" lesson as duplicate headers, and for the same reason.)

Path params vs query params, as design culture: the path identifies which resource (/users/42), the query modifies how you want it (?fields=name&format=json). Nothing enforces it; every well-designed API follows it; now your router makes the distinction natural.

Your exercise: Parameterized Routing

Patterns registered, requests in; handler + extracted params (path and query) out. The ladder: single :param → multiple → literal-vs-pattern precedence → segment-count mismatches → query parsing with decoding, empties, and duplicates per spec. When it passes, you've built the front door of Express/Flask/Fiber — the part of every framework that turns a URL into a function call with arguments, which is to say: the part that makes the web programmable.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…