URL Encoding Explained — Why %20 Means a Space
Why URLs can't contain arbitrary text, what percent-encoding really does, and the difference between encodeURI and encodeURIComponent.
You've probably seen %20 in a URL and wondered what it is. That's URL encoding doing its job — replacing a space with a percent code so URLs stay valid. The full system has more nuance than that, and getting it wrong causes some of the most common form bugs.
Why URLs need encoding
URLs have structure: scheme://host/path?query#fragment. Several characters have special meaning:
?separates the path from the query string.#introduces the fragment (page anchor).&separates query parameters.=separates key from value./separates path segments.- Space,
",<,>aren't allowed at all.
When you want to include these characters as data in a URL (e.g. searching for "C++" or a user's email "ada@example.com"), you need to encode them so they don't get interpreted as structure.
How percent encoding works
Every character is represented by its UTF-8 byte values, each in hexadecimal, prefixed by %.
| Character | Encoded | Why |
|---|---|---|
| space | %20 | Spaces not allowed in URLs |
| & | %26 | Reserved (query separator) |
| = | %3D | Reserved (key=value) |
| ? | %3F | Reserved (query start) |
| # | %23 | Reserved (fragment) |
| + | %2B | Often means space in queries |
| % | %25 | Must itself be escaped |
| ñ | %C3%B1 | Non-ASCII (2 UTF-8 bytes) |
| ✨ | %E2%9C%A8 | Emoji (3 UTF-8 bytes) |
encodeURI vs encodeURIComponent
JavaScript provides two functions, and confusing them is the #1 URL bug:
encodeURI
Designed to encode a full URL. Preserves URL structure characters: :, /,?, #, =, &.
encodeURI("https://example.com/search?q=hello world")
// "https://example.com/search?q=hello%20world"Use when you have a complete URL and just want to escape unsafe characters.
encodeURIComponent
Designed to encode a single component (one query parameter value, one path segment). Escapes everything that isn't a basic letter/digit/mark.
encodeURIComponent("hello & welcome")
// "hello%20%26%20welcome"
encodeURIComponent("https://example.com")
// "https%3A%2F%2Fexample.com"Use when you're constructing URL parts piece by piece. The vast majority of the time, you want this one.
The rule of thumb
- Building a URL string by concatenating parts? Use encodeURIComponent on each part.
- Already have a complete URL and just want to fix unsafe characters? Use encodeURI.
- When in doubt, use encodeURIComponent. It's safer to over-encode than under-encode.
The query string format
Query strings have their own format: ?key=value&key2=value2. Some specifics:
- Spaces in query values are typically encoded as
+(not%20) in form-urlencoded format. URLSearchParams handles both. - Multiple values for one key can be repeated:
?tag=a&tag=b. Or sometimes comma-separated:?tags=a,b. Conventions vary. - Arrays and objects have no single standard — different frameworks encode them differently. Common patterns:
arr[]=1&arr[]=2(PHP),arr=1&arr=2(Express),arr=1,2(Stripe).
The URL Encoder parses query strings and shows decoded key/value pairs in a readable table.
The + vs %20 wrinkle
In query strings, + traditionally means space (legacy HTML form encoding). In path segments, + means a literal plus sign.
This causes bugs when you copy a URL from one place to another. Searching for "C++" in a form might send ?q=C%2B%2B (correct) or ?q=C++ (wrong on server side — interpreted as "C ").
Modern best practice: always use %20 for spaces and %2B for plus, even in query strings. URLSearchParams does this correctly.
UTF-8 in URLs
Modern URLs support Unicode via percent-encoded UTF-8 bytes. The character "café" (where é is one character) encodes as caf%C3%A9 (5 bytes after encoding).
Browsers display Unicode in the address bar but transmit the percent-encoded form. If you seecaf%C3%A9 in a log, that's "café" — perfectly normal.
Common bugs
- Encoding a full URL with encodeURIComponent.
https://example.combecomeshttps%3A%2F%2Fexample.com— broken if used as a link. - Double encoding. Encoding an already-encoded value.
%20becomes%2520. Symptom: spaces in URLs literally read "%20". - Forgetting to encode user input. User types & in a comment, comment URL breaks.
- Manually building query strings. Always use
URLSearchParamsin browsers,querystringmodule in Node.
JavaScript quick reference
// Build a URL safely
const url = new URL("https://example.com/search")
url.searchParams.set("q", "hello world")
url.searchParams.set("filter", "a & b")
console.log(url.toString())
// "https://example.com/search?q=hello+world&filter=a+%26+b"
// Parse a query string
const params = new URLSearchParams("?q=hello+world&page=2")
params.get("q") // "hello world"
params.get("page") // "2"
// Manual encoding (when not using URL object)
const encoded = encodeURIComponent("foo & bar")
const decoded = decodeURIComponent("foo%20%26%20bar")Server-side considerations
Different servers and frameworks decode URLs at different points. By the time your handler runs:
- Express has decoded query parameters via
req.query. - Path parameters via
req.paramsare decoded. - The raw URL is in
req.url(still encoded). - Headers like Authorization are NOT URL-decoded — they have their own format.
Test what you encode
Before shipping a feature that constructs URLs from user input, test with the obvious edge cases:
- Spaces in input.
- Special characters: &, =, ?, #, %, /, +.
- Non-ASCII: ñ, ✨, 中.
- Empty strings.
- Already-encoded strings (does your code double-encode?).
Quick conversions: URL Encoder / Decoder — encode user input, decode logs, parse query strings into tables. Pair with the Base64 when you need binary-safe encoding.
Free tools for Developer
Put this article into practice — every tool is AI-powered, free, no sign-up.