How to Encode a Query String Correctly, Key by Key
Building a query string correctly means encoding the parts before you join them, never the joined string afterward. A query string is a sequence of key-value pairs, each pair joined by = and separated from the next by &. Those two characters, = and &, are structural delimiters under RFC 39861, so if a key or a value contains one, encoding the whole assembled string at the end is already too late: the delimiter has already done its damage to the structure before you got the chance to escape it.
The correct order is to percent-encode each key and each value independently with encodeURIComponent, then join the encoded pieces with = and &. Consequently, any & or = that was part of your data survives as %26 or %3D, while the delimiters you intentionally added stay literal.2 CapyToolkit's decode mode parses a query string into an editable table using exactly this per-value logic, which is the same discipline you want in your own code.
Why encoding the whole query string is wrong
Encoding an assembled query string treats your delimiters as if they were data, which breaks the string in the opposite direction from not encoding at all. If you build q=cats&page=2 first and then run the entire thing through encodeURIComponent, every & and = gets escaped, including the ones you deliberately placed to separate parameters.3
Consequently, the result, q%3Dcats%26page%3D2, is no longer a valid query string at all; it is a single opaque blob that a server reads as one meaningless value. The mistake is subtle because the function did exactly what it promises, escaping every character outside the unreserved set, but it was applied at the wrong stage. The fix is to change when you encode, not which function you use: encode each piece before assembly, never the assembled whole. Once you internalize that rule, the same discipline applies whether you are building a query string by hand, through a templating helper, or inside a shared library used across a whole codebase.
What happens when a server reads the blob
When a parser receives q%3Dcats%26page%3D2, it sees no literal = or &, so it cannot split the string into separate parameters. The most common outcome is that the server drops the query entirely or treats it as a single malformed key with an empty value. Seeing this in a browser history or analytics report is confusing because the string still looks like a URL at a glance. The only reliable way to recognize it is to decode the full string yourself and notice that the structural characters have been replaced by percent-escapes.
Correcting the mistake is straightforward once you understand the order: identify the original keys and values, encode each one separately with encodeURIComponent, and then rejoin them with literal = and &. This kind of query-string data recovery after an encoding mistake usually takes seconds and immediately turns the unusable blob back into a parsable query string. Maintaining the encode-then-join habit prevents the entire class of bug from recurring.
The correct build order: encode, then join
Building a correct query string follows a fixed three-step order, and skipping any of those steps is the most common reason URLs arrive at servers with the wrong number of parameters or with values that appear empty. First, take each key and value as plain, un-encoded text. Second, run encodeURIComponent on the key and on the value separately, before either of them ever touches a = or & that was meant to structure the query.4
Joining the encoded pieces
Third, join each encoded key to its encoded value with a literal =, and join each resulting pair to the next with a literal &. Following this order, a key user name and value A&B become user%20name=A%26B, with the delimiter characters you added remaining literal while the data character inside the value stays safely escaped. Furthermore, this is exactly what URLSearchParams does internally when you call its .toString() method5, so reaching for that built-in class instead of hand-assembling strings removes the chance of getting the order wrong. Sticking to this three-step order also makes code review easier, since a reviewer can check each step in isolation rather than reasoning about one long concatenation.
Rebuilding and editing with the tool
The CapyToolkit decoder mirrors this per-value discipline whenever you paste a URL containing a query string. Each parameter is split into its decoded key and decoded value and shown in an editable table, so you see the human-readable data rather than the escaped form behind a mask of percent-sign sequences. That split avoids the mental overhead of decoding percent-escapes while simultaneously tracing where one parameter ends and the next begins, especially in long query strings with many keys.
Editing a value without breaking the rest
Building on this, editing any value in that table triggers a rebuild that re-encodes only the changed value with encodeURIComponent and reassembles the full query string with & and = in their correct, literal positions. This means you can safely insert a value containing an ampersand, a plus sign, or non-ASCII text directly into the table, and the rebuilt query string below will encode exactly that value correctly, without you needing to manually escape anything or risk encoding the whole string by mistake. That per-field rebuild is the same discipline described above, made visible so you can watch the delimiters stay literal while only the data changes.
When to use this
Use this construction order whenever you build a query string manually in code, debug an API request that arrives with the wrong number of parameters, or need to insert a value that might contain &, =, or non-ASCII characters. When in doubt, prefer a built-in serializer like URLSearchParams over string concatenation, and use CapyToolkit to verify that a query string you already have decodes into the parameters you expect. Treating query-string construction as a per-value operation rather than a single string edit is the habit that prevents this entire class of bug from recurring.
Examples
Correct: encode each value first
key: user name, value: A&B
user%20name=A%26B
Each piece is encoded before the = and & are added, so the delimiters stay literal.
Incorrect: encode the whole string
user name=A&B
user%20name%3DA%26B
Encoding the assembled string escapes the intended = and & delimiters, producing an unusable single blob.
Using URLSearchParams to build it safely
new URLSearchParams({q: 'cats & dogs'}) q=cats+%26+dogs
The built-in class encodes each value correctly and joins them with the right delimiters automatically.
Editing a value in the parameter table
q=cats
q=cats%26dogs
Typing "cats&dogs" into the decoded value field rebuilds the query string with the ampersand safely escaped.
- 1.
IETF, "Reserved Characters," RFC 3986 Section 2.2, datatracker.ietf.org, January 2005. https://datatracker.ietf.org/doc/html/rfc3986#section-2.2
- 2.
WHATWG, "URL Standard," url.spec.whatwg.org, accessed July 2026. https://url.spec.whatwg.org/
- 3.
Mozilla Developer Network, "encodeURIComponent()," developer.mozilla.org, accessed July 2026. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
- 4.
ECMA, "encodeURIComponent (uriComponent)," ECMAScript 2027 Language Specification, Section 18.2.6.1, tc39.es, July 2026. https://tc39.es/ecma262/#sec-encodeuricomponent-uricomponent
- 5.
Mozilla Developer Network, "URLSearchParams," developer.mozilla.org, accessed July 2026. https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams