Encoding and special characters
URLs can only contain certain characters. Letters, numbers, and a handful of symbols are fine. But spaces, ampersands, question marks, and many other characters have special meaning or simply aren’t allowed. So what happens when your data contains them?
Let’s find out by breaking something.
The problem
Imagine you want to search for shoes & boots. If you just paste that into a URL as-is:
https://shop.com/search?query=shoes & boots This breaks. The browser sees:
query=shoesas a parameter with the value"shoes"- A space, which is invalid in URLs
& bootsas a new parameter name calledbootswith no value
The & has special meaning in URLs (it separates parameters), so you can’t use it literally inside a value. The space is simply not allowed. Your single search term just got torn apart into two broken pieces.
Percent-encoding
The solution is percent-encoding (also called URL encoding). Special characters are replaced with a % followed by their two-digit hexadecimal code:
| Character | Encoded as |
|---|---|
| space | %20 or + |
& | %26 |
= | %3D |
? | %3F |
# | %23 |
/ | %2F |
+ | %2B |
So shoes & boots becomes shoes+%26+boots in a query string. The space becomes + and the & becomes %26. Now the browser can see that the entire thing is a single value, not two separate parameters.
Automatic encoding with URLSearchParams
The best part about URLSearchParams? It handles encoding automatically. You never have to think about it:
const params = new URLSearchParams();
params.set("query", "shoes & boots");
params.set("filter", "price < 100");
console.log(params.toString());
// "query=shoes+%26+boots&filter=price+%3C+100" The & in “shoes & boots” became %26. The < in “price < 100” became %3C. The spaces became +. URLSearchParams did all of this without you asking.
And when you read the value back, it’s automatically decoded:
params.get("query"); // "shoes & boots" ← clean, original value!
params.get("filter"); // "price < 100" You never see the encoded version when using .get(). Encoding only exists in the serialized string form (.toString()). When you read the data back, you get the original, human-readable version.
Automatic encoding with URL
The URL object also handles encoding in pathnames:
const url = new URL("https://example.com");
url.pathname = "/products/running shoes";
console.log(url.href);
// "https://example.com/products/running%20shoes" The space in “running shoes” was automatically encoded as %20. You assigned a readable string, and the URL object took care of making it valid.
Manual encoding functions
Sometimes you need manual control over encoding. JavaScript provides two pairs of functions for this.
encodeURIComponent() / decodeURIComponent()
Use these for encoding individual values, like a single query parameter value or a single path segment:
encodeURIComponent("hello world & more");
// "hello%20world%20%26%20more"
decodeURIComponent("hello%20world%20%26%20more");
// "hello world & more" encodeURIComponent encodes everything that isn’t a basic letter, number, or one of a few safe characters (-, _, ., ~). It’s aggressive, and that’s the point. When you’re encoding a single value, you want to make sure nothing in it could be misinterpreted as URL structure.
encodeURI() / decodeURI()
Use these for encoding full URLs. They preserve characters that have structural meaning in URLs, like :, /, ?, #, and &:
encodeURI("https://example.com/path with spaces?q=hello world");
// "https://example.com/path%20with%20spaces?q=hello%20world" The spaces got encoded, but the ://, /, and ? were left alone because they’re part of the URL structure.
The key difference
encodeURIComponent("a=1&b=2"); // "a%3D1%26b%3D2" ← encodes = and &
encodeURI("a=1&b=2"); // "a=1&b=2" ← keeps = and & encodeURIComponent encodes everything special. encodeURI preserves URL structure characters. Use the wrong one and you’ll either break URL structure or fail to encode dangerous characters.
Rule of thumb
| Situation | What to use |
|---|---|
| Building any URL | Use the URL class (best option) |
| Working with query params | Use URLSearchParams (handles encoding for you) |
| Encoding a single value manually | Use encodeURIComponent() |
| Encoding a full URL string | Use encodeURI() |
In practice, if you’re using URL and URLSearchParams, you almost never need the manual functions. They exist for edge cases where you’re building URL strings by hand.
The + vs %20 confusion
In query strings, spaces can be encoded as either + or %20. Both are valid:
?query=hello+world ← valid
?query=hello%20world ← also valid URLSearchParams uses + for spaces in query strings. The URL pathname uses %20. This follows the web standards. You don’t need to choose between them. The tools pick the right one based on context.
This is one reason why @hectoday/http has its own custom query parser (which we’ll look at next). It needs to handle + as spaces when decoding query values:
// From @hectoday/http source:
const normalized = value.replace(/\+/g, " "); That one line converts every + back into a space before decoding the rest. It’s a small detail, but getting it wrong means your users’ search queries would contain literal + symbols instead of spaces.
With encoding covered, we’re ready to look inside @hectoday/http itself. In the next lesson, you’ll see the actual query parsing code the framework uses and understand exactly why it works the way it does.
What does new URLSearchParams({ q: 'a & b' }).get('q') return?