URL encoding is one of those things you use every day without thinking about it, right up until a query parameter with an ampersand in it silently breaks your API call. Then you spend an hour staring at logs. I've been there more than once, so let's cover what's actually going on.

What URL encoding is

URLs can only contain a limited set of ASCII characters. The spec (originally RFC 3986) reserves certain characters for structural meaning — things like ?, &, =, /, and #. Everything else that isn't in the safe set has to be represented differently.

Percent-encoding is the answer. Any byte that isn't allowed gets replaced with a % followed by its two-digit hexadecimal value. A space becomes %20. An ampersand becomes %26. A UTF-8 character like é becomes %C3%A9 — two bytes, so two percent-escapes.

That's the whole idea. It's a way to smuggle arbitrary text through a transport format that only tolerates a small alphabet.

When you actually need it

The most common case is building query strings from user input. If someone searches for salt & pepper, you can't just drop that after ?q=. The & would be read as a parameter separator and your backend would see two broken params.

  • Query parameters — anything user-supplied or containing reserved characters.
  • Path segments — file names with spaces or slashes, IDs that contain special characters.
  • Form submissions — application/x-www-form-urlencoded bodies are encoded the same way (with one twist, below).
  • Redirect URLs — when you pass a whole URL as a parameter, like ?return_to=..., the inner URL must be encoded so its own ? and & don't leak out.

The gotchas that cost real time

encodeURI vs encodeURIComponent

JavaScript gives you two functions and they are not interchangeable. This trips up almost everyone at some point.

  • encodeURI() is for encoding a whole URL. It leaves reserved characters like ?, &, /, and : alone because they're structurally meaningful.
  • encodeURIComponent() is for encoding a single piece — a parameter value. It escapes those reserved characters too.

Rule of thumb: if you're encoding one value that goes into a larger URL, use encodeURIComponent. If you use encodeURI on a value, an embedded & will pass through unescaped and break everything.

Spaces: %20 or +?

In the query string of a URL, a space encodes to %20. But in application/x-www-form-urlencoded data — the classic HTML form POST body — a space is often encoded as +. That's a legacy quirk from the form spec. When you decode form data you have to convert + back to a space; when you decode a normal URL you don't. Mixing these up produces literal + signs where you expected spaces, or vice versa.

Double encoding

This is the sneaky one. If you encode a string that's already encoded, %20 becomes %2520 (because the % itself gets escaped to %25). Users end up seeing %20 as literal text on the page. It usually happens when a value passes through two layers that both "helpfully" encode. When you see %25 creeping into your data, suspect double encoding.

Encoding the whole thing at once

Don't build the full URL string and then run it through an encoder. You'll destroy the structure. Encode each component separately, then assemble.

A worked example

Say you want to link to a search results page and pass along a filter. The raw values:

base: https://api.example.com/search
q:    coffee & cream
tags: 100% arabica

Encode each value on its own, then build the string:

const base = "https://api.example.com/search";
const params = new URLSearchParams({
  q: "coffee & cream",
  tags: "100% arabica",
});

const url = `${base}?${params.toString()}`;
// https://api.example.com/search?q=coffee+%26+cream&tags=100%25+arabica

Notice URLSearchParams did the right thing: it encoded the & to %26, the percent sign to %25, and used + for spaces (form-style, which servers handle fine for query strings). You didn't have to think about any of it — which is exactly why you should reach for a built-in tool instead of hand-rolling string concatenation.

If you're doing it manually, the equivalent would be:

const url = `${base}?q=${encodeURIComponent("coffee & cream")}`
         + `&tags=${encodeURIComponent("100% arabica")}`;
// ...?q=coffee%20%26%20cream&tags=100%25%20arabica

Both are valid. The server decodes them back to the original values either way.

Decoding, and checking your work

Decoding is the reverse: decodeURIComponent() in JS, or the language equivalent everywhere else. The one thing to watch is that decoding a malformed string (a stray % with no valid hex after it) will throw, so wrap it if the input is untrusted.

When I'm debugging a weird URL — a payment redirect, an OAuth callback, a deeply nested return_to — I paste it into a quick URL Encoder / Decoder to see the decoded form clearly. It's a lot faster than opening a REPL to run decodeURIComponent for the third time, and it saves you from misreading a %2F as an actual slash at a glance.

Quick checklist

  • Encode individual components, never the assembled URL.
  • Use encodeURIComponent for values, encodeURI for full URLs.
  • Remember spaces are %20 in URLs but + in form bodies.
  • If you spot %25 where you didn't expect it, you've probably double-encoded.
  • Prefer URLSearchParams (or your platform's query builder) over manual string joins.

Get these right and a whole category of intermittent "why is this parameter empty" bugs just disappears. Percent-encoding isn't complicated — it's just unforgiving about the details.

Photo by Chris Ried on Unsplash