Skip to main content
{/}codefunc
Web & APIJuly 22, 2026 · 6 min read

URL Anatomy and Percent-Encoding: Query Strings Without the Bugs

Break down URL parts, encode query values correctly, and know when to use encodeURIComponent vs encodeURI — plus reserved characters that break APIs.

By codefunc

urlencodingquery-stringweb-apihttppercent-encoding

Broken redirects, double-encoded query params, and "works in the browser but fails in fetch" bugs usually trace back to URL structure — not application logic. A URL is not a opaque string; it is a grammar with reserved characters and encoding rules. This guide dissects each part, shows how query strings should be built, and clarifies encodeURIComponent versus encodeURI.

Bottom line: Parse before you mutate. Encode values with encodeURIComponent (or URLSearchParams); never encode an entire URL with encodeURIComponent. Inspect structure in a URL Parser, build or parse params with the Query String Builder, and test encodings with URL Encode/Decode.

The parts of a URL

https://user:pass@www.example.com:443/path/to/page?query=1&sort=asc#section
└─┬─┘   └─┬─┘     └──────┬──────┘ └┬┘ └────┬────┘ └──────┬──────┘ └───┬───┘
scheme  userinfo       host      port   path         query         fragment
Part Example Notes
Scheme https Protocol; affects default port
Userinfo user:pass Rare in modern apps; avoid embedding passwords
Host www.example.com Domain or IP
Port 443 Omitted when default for scheme
Path /path/to/page Hierarchical segments
Query query=1&sort=asc Key/value; order may or may not matter to your app
Fragment section Client-only; not sent to the server

Paste any production URL into a URL Parser to see these fields split cleanly — especially when debugging redirects that drop the query or hash.

Absolute vs relative

https://example.com/tools/json-formatter   → absolute
/tools/json-formatter                      → path-absolute
?page=2                                    → query-relative to current URL
#faq                                       → fragment-only

fetch('/api') resolves against the current origin. new URL('/api', 'https://example.com') makes resolution explicit — prefer that in non-browser tooling.

Query strings that survive contact with reality

Canonical form:

?search=json+formatter&page=2&tags=css&tags=html

Rules that prevent bugs:

  1. Keys and values should be percent-encoded.
  2. Use & between pairs; = between key and value.
  3. Spaces become %20 (or + in application/x-www-form-urlencoded bodies — know which context you are in).
  4. Repeated keys may mean arrays — agree with your backend.

Prefer URL and URLSearchParams

const url = new URL("https://example.com/search");
url.searchParams.set("q", "css clamp()");
url.searchParams.set("page", "1");
console.log(url.toString());
// https://example.com/search?q=css+clamp%28%29&page=1

URLSearchParams encodes values correctly. Manual string concat is how % and & inside values corrupt the query.

// Fragile
const bad = `https://api.example.com/items?name=${name}&note=${note}`;

// Safe
const url = new URL("https://api.example.com/items");
url.searchParams.set("name", name);
url.searchParams.set("note", note);

encodeURIComponent vs encodeURI

Both are JavaScript helpers for percent-encoding. They are not interchangeable.

Function Encodes spaces etc. Leaves intact Use for
encodeURIComponent Yes A-Z a-z 0-9 - _ . ! ~ * ' ( ) Single query value, path segment, or form field
encodeURI Yes Reserved URL punctuation: ; , / ? : @ & = + $ # among others Rarely — almost whole URLs that are already structured
encodeURIComponent("a&b=c");
// "a%26b%3Dc"

encodeURI("https://example.com/a&b=c");
// "https://example.com/a&b=c"  — & not encoded; still a broken query if you meant one value

Common mistake: encoding the whole URL

// Wrong — breaks the URL
fetch(encodeURIComponent("https://example.com/api?q=1"));

// Right — encode only the value
fetch("https://example.com/api?q=" + encodeURIComponent("1 & 2"));

// Better
const u = new URL("https://example.com/api");
u.searchParams.set("q", "1 & 2");
fetch(u);

Round-trip experiments belong in URL Encode/Decode: encode a value, decode it, confirm you did not double-encode (%2526 instead of %26).

Reserved and unsafe characters

RFC 3986 reserved characters have special meaning in URLs:

:/?#[]@!$&'()*+,;=

If a value must contain them, percent-encode. If you leave & raw inside a value, parsers treat it as a separator.

Character Often encoded as Why
Space %20 Separators / readability
& %26 Query pair delimiter
= %3D Key/value delimiter
# %23 Starts fragment
? %3F Starts query
/ %2F Path segment boundary (when inside a segment)
% %25 Escape character itself
+ %2B Ambiguous with form space-as-plus

Path segments vs query

// Path segment with a slash in the id — encode the segment
const id = "a/b";
const path = `/items/${encodeURIComponent(id)}`;
// /items/a%2Fb

Slugs for blogs and tools are a different concern: human-readable path segments usually restrict to safe characters. A Slug Generator turns titles into lowercase-hyphenated paths so you avoid encoding surprises in public URLs.

Double encoding and decoding bugs

Symptoms:

  • Logs show %253A instead of %3A (encoded % then :).
  • Server receives literal %20 instead of a space.
  • Redirect loops that re-encode an already-encoded Location.

Discipline:

  1. Keep data decoded in application memory.
  2. Encode once at the HTTP boundary (building a URL or header).
  3. Decode once when parsing a request.
  4. Never run encodeURIComponent on a string that is already a full encoded URL.

Use a URL Parser to see whether a mysterious string is a full URL or a lone encoded value.

Fragments, tracking params, and privacy

  • Fragments (#...) stay in the browser — useful for in-page navigation; useless for server analytics.
  • UTM and referral params pollute canonical URLs — strip them for rel=canonical and sharing when they are not part of content identity.
  • Tokens in query strings leak via Referer, logs, and browser history — prefer headers or fragments carefully (fragments still appear in history).

Internationalized domains and paths

User-facing URLs may include non-ASCII characters. Under the hood:

Layer Encoding
Domain (IDN) Punycode in DNS (bücher.dexn--bcher-kva.de)
Path / query UTF-8 percent-encoding (%C3%A9 for é)
const url = new URL("https://example.com/");
url.pathname = "/café";
url.searchParams.set("q", "naïve");
console.log(url.href);
// https://example.com/caf%C3%A9?q=na%C3%AFve

When comparing URLs in tests, normalize with the URL API rather than raw string equality — browsers may serialize the same resource differently (%7E vs ~).

Debugging checklist

  • Parse with URL Parser — confirm path vs query vs hash
  • Encode a suspect value in URL Encode/Decode; check for %25 double-encoding
  • Rebuild the URL with URLSearchParams and compare to the hand-built string
  • Verify the server framework's query parser (some decode once, some twice)
  • For public content paths, generate a clean slug with Slug Generator instead of embedding raw titles

Practical takeaway

Treat URLs as structured data: scheme, host, path, query, fragment. Build queries with URL / URLSearchParams, encode individual values with encodeURIComponent, and leave encodeURI alone unless you know why you need it. Debug with a URL Parser and URL Encode/Decode; create safe public paths with a Slug Generator. Encode once at the edge — never trust hand-concatenated strings with user input.

Sources

Try it on codefunc

Related articles

3

Search tools

Find a developer tool