URL Builder
Assemble full URLs from parts—scheme, host, path, query keys and values—with automatic encoding.
URL Breakdown:
Assemble a URL from a scheme, host, path, parameters and fragment with the encoding handled for you. The mechanical part is straightforward. The part that breaks is that no specification says how to put a list or a nested object into a query string, so every framework invented its own answer — and the receiving parser has to be told which one you picked.
No specification defines arrays in a query string
RFC 3986 §3.4 defines the query as everything between the first ? and a #, composed of pchar plus / and ?. That is the entire definition. Key-value pairs joined by & are a convention inherited from HTML form submission, not from URI syntax, and nothing in either document says what a list should look like.
Four conventions are in wide use: a repeated key (tag=a&tag=b), an empty-bracket suffix (tag[]=a&tag[]=b), an explicit index (tag[0]=a&tag[1]=b), and a comma-joined single value (tag=a,b). Brackets are gen-delims in RFC 3986 and are not legal unencoded, so in practice they travel as %5B and %5D.
Defaults differ even within one ecosystem. The qs library, which Express uses when its query parser is set to extended, defaults to the indexed form and emits a%5B0%5D=1&a%5B1%5D=2; its arrayFormat option switches to repeat, brackets or comma. OpenAPI 3 uses a different vocabulary again, with style and explode. Pick one convention and document it, because the parser at the other end will not infer it.
URLSearchParams is a form serialiser, not a URI serialiser
URLSearchParams implements application/x-www-form-urlencoded serialisation, whose percent-encode set is everything except ASCII alphanumerics and *, -, . and _. That set is not the one encodeURIComponent uses, and in places it is inverted: URLSearchParams escapes ~ to %7E, ' to %27 and parentheses to %28 and %29, all of which encodeURIComponent leaves bare, while both leave * untouched.
Two functionally identical URLs can therefore differ byte for byte depending on which routine built them. That is invisible until something computes a cache key, an ETag or a signature over the serialised string, at which point the two sides stop matching for no visible reason.
The constructor coerces rather than validates. Building from an object whose values are undefined or null produces the literal strings a=undefined and b=null, not omitted keys — a common source of the string undefined reaching production APIs. An array value is joined with a comma and encoded as %2C. Two conveniences are worth knowing: the constructor strips one leading ?, so location.search can be passed in directly, and sort() orders pairs by name using code-unit comparison, which is the cheapest way to make cache keys stable across parameter ordering.
Nested objects hit limits you never configured
Bracket nesting such as filter[status]=open is the usual way to express structure, and qs parses it — but with defaults that quietly change the shape of your data rather than raising an error.
Depth is capped at five. Parsing a[b][c][d][e][f][g]=h yields five levels of nesting and leaves the literal string [g] as the final key instead of a sixth level. Arrays are capped by arrayLimit, which defaults to 20: any index of 20 or higher converts the whole array into a plain object keyed by index, so a[0]=x&a[25]=y parses to an object with keys 0 and 25 rather than an array. That conversion is deliberate — it prevents a[999999999] from allocating an enormous sparse array — but it means your handler receives a different type depending on how many items the caller sent.
parameterLimit defaults to 1000 pairs, and anything beyond that is discarded silently unless throwOnLimitExceeded is enabled. If your payload is genuinely nested, putting JSON into a single parameter or moving it to a request body sidesteps all three limits and the type ambiguity that produced them.
Duplicate keys resolve differently at every hop
A query string may legally repeat a key and nothing specifies which occurrence wins. The OWASP Web Security Testing Guide documents the split in its parameter-pollution section: ASP.NET and IIS concatenate every occurrence with a comma, PHP keeps the last, Java servlet containers keep the first, and Django and Flask hand you the entire list.
JavaScript makes the choice explicit instead of picking for you — get() on URLSearchParams returns the first occurrence and getAll() returns every one. That is safer, but it still means two call sites in one codebase can read the same URL differently.
The security consequence is that a WAF, a CDN and the application behind them are under no obligation to agree. A filter inspecting the first occurrence in front of a backend that reads the last is one duplicate key away from a bypass, and the OWASP guide is explicit that any table of defaults is a starting point to verify rather than a fact about your stack. When you build a URL, emit each key exactly once unless the array convention you chose requires otherwise, and reject unexpected duplicates on the receiving side.
Joining a base to a path, and the ceiling on the result
Constructing a URL from a relative part and a base applies RFC 3986 reference resolution, not string concatenation, and the first character of the relative part selects between three different behaviours. v1/users against https://api.test/a/b gives https://api.test/a/v1/users, replacing the base's last segment. /v1/users discards the base path completely. //other.test/v1 discards the host too. Keeping a base path prefix requires the base to end in a slash and the relative part not to begin with one.
The finished URL then has to survive every hop between you and the origin. Apache's LimitRequestLine defaults to 8190 bytes for the entire request line — method, full URI and protocol version together — and returns 414 above it. Proxies, load balancers and logging pipelines impose their own caps, usually lower than expected and rarely documented in the same place as the API.
Finally, remember where a query string travels. It is written to access logs at every hop and sent in the Referer header on outbound navigations. Session tokens, API keys and personal data belong in a header or a body, not in a parameter baked into a shareable link. To confirm the assembled URL parses into the components you intended, run it through the URL parser.
Related tools
Try these tools that work well with this one
JSON to Query String
Convert JSON objects to URL query strings for web development
URL Decoder
Free online URL decoder tool for decoding percent-encoded URLs
URL Encoder
Free online URL encoder tool for encoding special characters
URL Parser
Free URL parser tool that breaks down URLs into their components
URL Redirect Checker
Trace HTTP redirects, status codes, and headers
URL Validator
Free online URL validator tool to check URL validity
Related guides
Tutorials and guides to get more from this tool
The Art of URL Building: Best Practices for 2025
Master the construction of complex URLs. Learn about protocol relative URLs, proper parameter encoding, and avoiding common pitfalls.
JSON to Query String: simplifying API Requests in 2025
Learn how to convert nested JSON objects into URL-encoded query strings significantly simplifying your API development workflow.
URL Validation: Why Regular Expressions Aren't Enough
Validating URLs is harder than it looks. Explore why simple regex fails and how to ensure URLs are properly formatted and safe.
ThenCatch is a free developer toolkit. Learn more about us.