Every language
12 languages, copy-ready. One at a time with syntax highlighting, or all inline.
JSJavaScript
const url = new URL('https://x.test/search');
// build — set replaces, append repeats
url.searchParams.set('q', 'cafés & tea');
url.searchParams.append('tag', 'drinks');
url.searchParams.append('tag', 'sweet');
console.log(url.toString());
// https://x.test/search?q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
// parse
console.log(url.searchParams.get('tag')); // 'drinks' — first of many
console.log(url.searchParams.getAll('tag')); // ['drinks', 'sweet']
for (const [key, value] of url.searchParams) console.log(key, value);URLSearchParams encodes spaces as + (form encoding) — fine for query strings, wrong for path segments. get() hands you the first value only; getAll() is the multi-value form.
TSTypeScript
export function buildQuery(
params: Record<string, string | string[]>,
): string {
const sp = new URLSearchParams();
for (const [key, value] of Object.entries(params)) {
for (const v of Array.isArray(value) ? value : [value]) {
sp.append(key, v);
}
}
return sp.toString();
}
buildQuery({ q: 'cafés & tea', tag: ['drinks', 'sweet'] });
// 'q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet'Same URLSearchParams engine, typed — Record<string, string | string[]> is the honest shape for repeated keys; append (not set) so arrays survive as repeats.
GoGo
import (
"fmt"
"net/url"
)
// build
v := url.Values{}
v.Set("q", "cafés & tea")
v.Add("tag", "drinks")
v.Add("tag", "sweet")
fmt.Println(v.Encode()) // q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
// parse — url.Values is map[string][]string, repeats built in
q, _ := url.ParseQuery("q=x&tag=a&tag=b")
fmt.Println(q["tag"]) // [a b]Encode sorts keys alphabetically — order is NOT preserved. ParseQuery returns map[string][]string, so multi-value keys arrive as slices; a plain index on a missing key yields an empty slice, not nil-crash.
RsRust
use url::form_urlencoded::{parse, Serializer};
fn main() {
// build
let mut ser = Serializer::new(String::new());
ser.append_pair("q", "cafés & tea");
ser.append_pair("tag", "drinks");
ser.append_pair("tag", "sweet");
let query = ser.finish();
println!("{query}"); // q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
// parse — yields every (key, value) pair, repeats included
for (k, v) in parse(query.as_bytes()) {
println!("{k}={v}");
}
}The url crate's query_pairs_mut() is the mutate-a-URL-in-place form: url.query_pairs_mut().append_pair("tag", "x"). serde_urlencoded is the serde route when you want a struct instead of pairs.
PHPPHP
<?php
// build — repeated keys need the bracket form; encoding is handled
$query = http_build_query([
'q' => 'cafés & tea',
'tag' => ['drinks', 'sweet'],
]);
echo $query, PHP_EOL; // q=caf%C3%A9s+%26+tea&tag%5B0%5D=drinks&tag%5B1%5D=sweet
// parse — the result array is REQUIRED since PHP 8
parse_str($query, $params);
print_r($params['tag']); // ['drinks', 'sweet']parse_str without the second arg injects variables into scope — required (and removed otherwise) since PHP 8. And a plain repeated key (tag=a&tag=b) keeps only the LAST value; arrays come only from tag[] syntax, the PHP-ism other languages don't share.
PyPython
from urllib.parse import parse_qs, urlencode
# build — a list of pairs keeps order AND repeated keys (a dict cannot)
query = urlencode([
('q', 'cafés & tea'),
('tag', 'drinks'),
('tag', 'sweet'),
])
print(query) # q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
# parse
params = parse_qs(query)
print(params['tag']) # ['drinks', 'sweet']parse_qs returns a LIST per key and drops blank values unless keep_blank_values=True ('a=&b=1' loses 'a'). parse_qsl gives flat (key, value) pairs when you don't want the lists.
C#C#
using System.Text;
var builder = new UriBuilder("https://x.test/search");
var sb = new StringBuilder();
foreach (var (key, value) in new[] {
("q", "cafés & tea"),
("tag", "drinks"),
("tag", "sweet"),
})
{
if (sb.Length > 0) sb.Append('&');
sb.Append(Uri.EscapeDataString(key))
.Append('=')
.Append(Uri.EscapeDataString(value));
}
builder.Query = sb.ToString();
Console.WriteLine(builder.Uri);
// https://x.test/search?q=caf%C3%A9s%20%26%20tea&tag=drinks&tag=sweetUriBuilder + Uri.EscapeDataString is the portable no-ASP.NET build route (%20 style — both it and + decode fine). For parsing, HttpUtility.ParseQueryString (System.Web) returns a NameValueCollection whose indexer COMMA-JOINS repeated keys — GetValues("tag") is the multi-value form. Microsoft.AspNetCore.WebUtilities.QueryString is the modern alternative.
JvJava
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
public class BuildQuery {
// the JDK has no params type — join encoded pairs yourself
static String buildQuery(List<String[]> pairs) {
StringBuilder sb = new StringBuilder();
for (String[] kv : pairs) {
if (sb.length() > 0) sb.append('&');
sb.append(URLEncoder.encode(kv[0], StandardCharsets.UTF_8))
.append('=')
.append(URLEncoder.encode(kv[1], StandardCharsets.UTF_8));
}
return sb.toString();
}
public static void main(String[] args) {
String q = buildQuery(List.of(
new String[]{"q", "cafés & tea"},
new String[]{"tag", "drinks"},
new String[]{"tag", "sweet"}));
System.out.println(q); // q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
}
}URLEncoder is form encoding — spaces become +, never %20 — and it is encode-ONLY: java.net.URI has no query parser (the honest stdlib gap; getRawQuery() hands you the raw string). Spring's UriComponentsBuilder fills both directions.
SwSwift
import Foundation
var comps = URLComponents(string: "https://x.test/search")!
comps.queryItems = [
URLQueryItem(name: "q", value: "cafés & tea"),
URLQueryItem(name: "tag", value: "drinks"),
URLQueryItem(name: "tag", value: "sweet"),
]
print(comps.url!)
// https://x.test/search?q=caf%C3%A9s%20%26%20tea&tag=drinks&tag=sweet
// parse
let parsed = URLComponents(string: "https://x.test/search?tag=a&tag=b")!
for item in parsed.queryItems ?? [] {
print("\(item.name)=\(item.value ?? "")")
}URLComponents + URLQueryItem is the model Apple gives you. Percent-encoding follows RFC 3986 defaults — spaces become %20, not +. Assigning queryItems replaces the whole list (no append API), and two items with the same name both survive; group them yourself.
KtKotlin
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
fun buildQuery(pairs: List<Pair<String, String>>): String =
pairs.joinToString("&") { (k, v) ->
"${URLEncoder.encode(k, StandardCharsets.UTF_8)}=${URLEncoder.encode(v, StandardCharsets.UTF_8)}"
}
fun parseQuery(query: String): List<Pair<String, String>> =
query.split('&').filter { it.isNotEmpty() }.map { pair ->
val i = pair.indexOf('=')
if (i < 0) pair to ""
else URLDecoder.decode(pair.substring(0, i), StandardCharsets.UTF_8) to
URLDecoder.decode(pair.substring(i + 1), StandardCharsets.UTF_8)
}
fun main() {
val q = buildQuery(listOf("q" to "cafés & tea", "tag" to "drinks", "tag" to "sweet"))
println(q) // q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
println(URI("https://x.test/search?$q").rawQuery) // raw string, no splitting
parseQuery(q).forEach { (k, v) -> println("$k=$v") }Same JVM URLEncoder (+ for space) plus a small hand parse via URI.rawQuery splitting — the JDK ships no query parser, so the split-and-decode loop IS the recipe.
RbRuby
require 'uri'
# build — an array of [key, value] pairs keeps order and repeats
query = URI.encode_www_form([
['q', 'cafés & tea'], ['tag', 'drinks'], ['tag', 'sweet']
])
puts query # q=caf%C3%A9s+%26+tea&tag=drinks&tag=sweet
# parse
URI.decode_www_form(query).each { |k, v| puts "#{k}=#{v}" }CGI.escape is + -style; URI.encode_www_form_component matches it — the component form for one value, the array form for a whole query. Rack::Utils.parse_nested_query handles Rails-style tag[] nesting.
ZigZig
const std = @import("std");
fn appendEncoded(out: []u8, len: usize, s: []const u8) usize {
var i = len;
for (s) |c| {
const unreserved = std.ascii.isAlphanumeric(c) or
c == '-' or c == '_' or c == '.' or c == '~';
if (unreserved) {
out[i] = c;
i += 1;
} else {
out[i] = '%';
out[i + 1] = "0123456789ABCDEF"[c >> 4];
out[i + 2] = "0123456789ABCDEF"[c & 0xf];
i += 3;
}
}
return i;
}
pub fn main() !void {
// build — no params type in std; percent-encode into a buffer by hand
var buf: [256]u8 = undefined;
var len: usize = 0;
const pairs = [_][2][]const u8{
.{ "q", "cafes & tea" },
.{ "tag", "drinks" },
.{ "tag", "sweet" },
};
for (pairs, 0..) |pair, i| {
if (i != 0) {
buf[len] = '&';
len += 1;
}
len = appendEncoded(&buf, len, pair[0]);
buf[len] = '=';
len += 1;
len = appendEncoded(&buf, len, pair[1]);
}
std.debug.print("{s}\n", .{buf[0..len]});
// parse — std.Uri.parse handles the full URI; split the raw query yourself
const uri = try std.Uri.parse("https://x.test/search?q=x&tag=a&tag=b");
const query = uri.query orelse "";
var it = std.mem.splitScalar(u8, query, '&');
while (it.next()) |pair| std.debug.print("{s}\n", .{pair});
}std.Uri can parse (std.http uses it) but not build — the buffer loop IS the recipe. Deliberately sticks to stable std APIs (std.ascii, std.mem.splitScalar) so it survives the Io-writer churn across Zig releases.