DevTools Hub

Search tools

Search for a developer tool

How Query Parameters Work

Part of the Encoding Toolkit

Query parameters look simple — ?key=value — and mostly are, right up until you need a second value for the same key, or your framework silently drops one you sent. This is about the mechanics and conventions, not the encoding — see URL Encoding Explained if you need that part.

What a query string actually is

Everything after the ? in a URL, up to the # fragment (if any), is the query string — a flat sequence of key=value pairs joined by &:

https://example.com/search?q=laptops&sort=price&page=2
                  \_______________________________________/
                                query string

That's it structurally — there's no nesting, no types, no schema. Every key and every value is just a string, and it's entirely up to server-side code to decide what those strings are supposed to mean.

Everything is a string — always

?active=true doesn't give you a boolean; it gives you the two-character string "true". ?page=2 gives you the string "2", not the number 2. Server frameworks that expose typed query params (Express with a schema validator, most Rust/Go web frameworks) are doing that coercion for you — the wire format itself has no concept of type, so ?active=false and ?active=nonsense are equally "truthy" strings unless something explicitly checks the value.

Repeated keys — the part with no single standard

Query strings don't forbid using the same key twice, and there's no single standard for what that means — three different conventions are all common, and which one a given backend expects is a real thing you have to know:

ConventionExampleSeen in
Repeat the key?tag=js&tag=cssURLSearchParams (browser/JS), most simple query parsers
Bracket suffix?tag[]=js&tag[]=cssRails, PHP, and libraries like qs (common in Express APIs)
Comma-joined single value?tag=js,cssMany REST APIs, especially for filter parameters

Sending the wrong convention to a backend that expects another isn't usually an error — it just silently produces the wrong result. A backend expecting brackets that receives repeated bare keys will often see only tag as a single-value param, taking just one of the values (typically the last) and ignoring the rest, with nothing telling you it happened.

What "the first match wins" costs you

Many query APIs — URLSearchParams.get() included — return only the first value for a repeated key, not the last, not an error. If you actually want every value, you need the plural form: URLSearchParams.getAll(), not .get(). Reaching for .get() on a key that might be repeated is a quiet, easy-to-miss bug — the code runs fine, it just discards data.

Three different states, not two

A key can be present with a value, present with an empty value, or absent — and some frameworks distinguish all three where others collapse two of them:

  • ?filter=active — present, value "active"
  • ?filter= — present, value "" (empty string, not the same as absent)
  • (no filter param at all) — absent entirely

Whether ?filter= and a missing filter should behave the same is an API design decision, not something the query string format decides for you — worth being explicit about (and testing) rather than assuming.

Order is usually preserved, but don't rely on it

In practice, URLSearchParams and most server-side parsers preserve the order parameters were written in, including the relative order of repeated keys. But nothing in the HTTP or URL specs requires this, and a caching layer, proxy, or client library that reconstructs a URL from a parsed object is free to reorder it. Don't design an API where parameter order carries meaning — use distinct keys or an explicit index instead.

Query params vs. path segments vs. the request body

Query parameters aren't the only way to send data to a server, and mixing up when to use each is a common REST API design mistake:

  • Path segment (/users/42) — for identifying which resource, when the value is required and the URL is meaningless without it.
  • Query parameter (?sort=name&page=2) — for optional modifiers to a request: filtering, sorting, pagination, search terms. If a request works fine without the parameter (using some default), it belongs in the query string, not the path.
  • Request body — for data that's the actual payload of a POST/PUT/PATCH, or that's too large, structured, or sensitive to put in a URL. URLs get logged by proxies, browser history, and server access logs by default — the query string is not a private channel.

There's a practical length limit

Nothing in the URL spec sets a hard maximum length, but real infrastructure does — most browsers and servers start rejecting or truncating somewhere around 2,000–8,000 characters depending on the component (Internet Explorer's old 2,083-character limit is the most conservative number still floating around as folklore, though most modern stacks tolerate more). A query string built from a large array of IDs or a bulk filter list is a common way to hit this — the practical fix is almost always to move that data into a POST body instead of trying to fit it in the URL.

Common conventions worth reusing

  • Pagination: page + limit (or pageSize), or offset + limit
  • Sorting: sort=price, or sort=-price / sort=price:desc for direction
  • Search: a single q parameter, by wide convention
  • Filtering: one parameter per field (status=active), or a namespaced form (filter[status]=active) for larger filter sets

None of these are enforced by any spec — they're conventions borrowed from widely used APIs, which is exactly why following them makes an API easier for other developers to guess correctly without reading the docs.

Try it yourself

Break an existing query string down into its parameters — repeated keys grouped as arrays — with Query String Parser, or build one from scratch, including array values, with Query Parameter Builder. To work with a full URL rather than just the query part, see URL Parser. All three run entirely in your browser.

Related tools