URL #
The URL (Uniform Resource Locator) is the fundamental interface of the web — every HTTP request, every link, every API endpoint is represented as a URL. Correct URL manipulation isn’t just string concatenation — there’s special character encoding, path normalization, query parameter handling, and many edge cases that are easy to get wrong when done manually. Node.js provides URL and URLSearchParams as built-in APIs following the WHATWG URL standard — exactly the same as used in browsers, so the code you write on the server works in the browser without modification.
URL Anatomy #
Before writing code, it’s important to understand the components that make up a URL.
https://user:[email protected]:8080/v1/produk?q=laptop&halaman=2#hasil
│────│ │────────│ │─────────────│ │──│ │───────│ │─────────────────│ │───│
│ │ │ │ │ │ │ │ │ │ │ │ │ │
protocol credential hostname port pathname search (query) hash
(auth)
const url = new URL("https://user:[email protected]:8080/v1/produk?q=laptop&halaman=2#hasil");
console.log(url.protocol); // "https:"
console.log(url.username); // "user"
console.log(url.password); // "pass"
console.log(url.hostname); // "api.example.com"
console.log(url.port); // "8080"
console.log(url.host); // "api.example.com:8080" (hostname + port)
console.log(url.pathname); // "/v1/produk"
console.log(url.search); // "?q=laptop&halaman=2"
console.log(url.hash); // "#hasil"
console.log(url.origin); // "https://api.example.com:8080"
console.log(url.href); // the full URL as a string
Creating and Parsing URLs #
The URL Constructor #
URL accepts an absolute URL string, or a relative URL with a base URL as the second argument.
// absolute URL
const url1 = new URL("https://example.com/path?q=hello");
// relative URL against a base
const url2 = new URL("/v1/users", "https://api.example.com");
console.log(url2.href); // "https://api.example.com/v1/users"
const url3 = new URL("../images/logo.png", "https://example.com/assets/css/");
console.log(url3.href); // "https://example.com/assets/images/logo.png"
// ANTI-PATTERN: string concatenation to build URLs
function getEndpointSalah(baseUrl: string, path: string, id: string): string {
return baseUrl + "/" + path + "/" + id; // ✗ unsafe, no normalization
}
// CORRECT: use the URL API
function getEndpoint(baseUrl: string, path: string, id: string): string {
return new URL(`${path}/${id}`, baseUrl).href; // ✓ automatic normalization
}
// URL validation — URL() throws a TypeError if invalid
function isURLValid(urlString: string): boolean {
try {
new URL(urlString);
return true;
} catch {
return false;
}
}
console.log(isURLValid("https://example.com")); // true
console.log(isURLValid("bukan url")); // false
console.log(isURLValid("ftp://files.com")); // true
console.log(isURLValid("/relative/path")); // false — relative URL without a base
// validate http/https only
function isHTTPUrl(urlString: string): boolean {
try {
const url = new URL(urlString);
return url.protocol === "http:" || url.protocol === "https:";
} catch {
return false;
}
}
Modifying URL Components #
URL properties can be set directly — the URL is automatically updated and normalized.
const url = new URL("https://example.com/path");
// change the protocol
url.protocol = "http:";
console.log(url.href); // "http://example.com/path"
// change the hostname
url.hostname = "api.example.com";
console.log(url.href); // "http://api.example.com/path"
// change the port
url.port = "3000";
console.log(url.href); // "http://api.example.com:3000/path"
// add a path
url.pathname = "/v2/users";
console.log(url.href); // "http://api.example.com:3000/v2/users"
// remove the port — set it to an empty string
url.port = "";
console.log(url.href); // "http://api.example.com/v2/users"
// add a hash
url.hash = "section-1";
console.log(url.href); // "http://api.example.com/v2/users#section-1"
URLSearchParams — Query Strings #
URLSearchParams is a dedicated API for managing query parameters. It handles encoding/decoding of special characters automatically.
// create from a query string
const params1 = new URLSearchParams("q=laptop+gaming&kategori=elektronik&halaman=1");
// create from an object
const params2 = new URLSearchParams({
q: "laptop gaming",
kategori: "elektronik",
halaman: "1",
});
// create from an array of pairs
const params3 = new URLSearchParams([
["q", "laptop gaming"],
["tag", "sale"],
["tag", "new"], // duplicate keys are allowed
]);
Query Parameter Operations #
const params = new URLSearchParams();
// set — replaces the value if the key already exists
params.set("q", "laptop");
params.set("halaman", "1");
params.set("perHalaman", "20");
// append — adds a new value even if the key exists
params.append("tag", "gaming");
params.append("tag", "sale"); // two values for the key "tag"
// get — takes the first value of the key
console.log(params.get("q")); // "laptop"
console.log(params.get("tag")); // "gaming" — only the first
console.log(params.get("kosong")); // null — key doesn't exist
// getAll — takes all values of the key
console.log(params.getAll("tag")); // ["gaming", "sale"]
// has — checks whether the key exists
console.log(params.has("q")); // true
console.log(params.has("kosong")); // false
// delete — removes all values of the key
params.delete("tag");
console.log(params.has("tag")); // false
// convert to a string
console.log(params.toString());
// "q=laptop&halaman=1&perHalaman=20"
// iterate all parameters
for (const [key, value] of params) {
console.log(`${key}: ${value}`);
}
// or with forEach
params.forEach((value, key) => {
console.log(`${key} = ${value}`);
});
// sort — order parameters alphabetically (useful for consistent cache keys)
params.sort();
console.log(params.toString());
// "halaman=1&perHalaman=20&q=laptop"
Integrating URLSearchParams with URL #
const url = new URL("https://api.example.com/produk");
// access searchParams from the URL object
url.searchParams.set("q", "laptop");
url.searchParams.set("kategori", "elektronik");
url.searchParams.append("tag", "gaming");
console.log(url.href);
// "https://api.example.com/produk?q=laptop&kategori=elektronik&tag=gaming"
// modify directly via searchParams
url.searchParams.delete("tag");
url.searchParams.set("halaman", "2");
console.log(url.search); // "?q=laptop&kategori=elektronik&halaman=2"
// read query parameters from an existing URL
const urlPencarian = new URL("https://shop.com/cari?q=laptop&hargaMax=5000000&stok=true");
const query = urlPencarian.searchParams.get("q"); // "laptop"
const hargaMax = Number(urlPencarian.searchParams.get("hargaMax")); // 5000000
const stokAda = urlPencarian.searchParams.get("stok") === "true"; // true
URL Encoding #
URLs can only contain certain ASCII characters. Other characters — spaces, non-ASCII characters, special characters — must be encoded in %XX format.
flowchart TD
A[Character in URL] --> B{Safe character\nfor URLs?}
B -- Yes --> C["Write as-is\nA-Z, a-z, 0-9, - _ . ~"]
B -- No --> D["Encode: %XX\nSpace → %20\nä → %C3%A4"]
D --> E[Valid URL]
C --> EencodeURIComponent vs encodeURI #
// encodeURIComponent — encodes everything except: A-Z a-z 0-9 - _ . ! ~ * ' ( )
// use for query parameter values or path segments
console.log(encodeURIComponent("laptop gaming")); // "laptop%20gaming"
console.log(encodeURIComponent("harga=10.000")); // "harga%3D10.000"
console.log(encodeURIComponent("nama/dengan/slash")); // "nama%2Fdengan%2Fslash"
console.log(encodeURIComponent("bahasa Indonesia")); // "bahasa%20Indonesia"
// encodeURI — encodes a full URL, but does NOT encode URL structural characters
// : / ? # [ ] @ ! $ & ' ( ) * + , ; =
// use for encoding an already-formed URL (rarely needed)
console.log(encodeURI("https://example.com/path dengan spasi?q=nilai"));
// "https://example.com/path%20dengan%20spasi?q=nilai"
// note: ? and = are not encoded because they're part of the URL structure
// ANTI-PATTERN: encoding query parameters with encodeURI
function buatURLSalah(base: string, keyword: string): string {
return encodeURI(`${base}?q=${keyword}`); // ✗ doesn't encode & = characters in the keyword
}
// CORRECT: use URLSearchParams which encodes automatically
function buatURL(base: string, params: Record<string, string>): string {
const url = new URL(base);
for (const [key, value] of Object.entries(params)) {
url.searchParams.set(key, value); // ✓ automatic encoding
}
return url.href;
}
console.log(buatURL("https://example.com/cari", {
q: "laptop & tablet", // & inside the value — encoded automatically
kategori: "elektronik=premium", // = inside the value — encoded automatically
}));
// "https://example.com/cari?q=laptop+%26+tablet&kategori=elektronik%3Dpremium"
// decode
console.log(decodeURIComponent("laptop%20gaming")); // "laptop gaming"
console.log(decodeURIComponent("harga%3D10.000")); // "harga=10.000"
URL Normalization #
Textually different URLs can refer to the same resource. Normalization ensures a consistent representation.
// URL() already does basic normalization
const url1 = new URL("HTTPS://Example.COM/PATH/../to/./resource");
console.log(url1.href);
// "https://example.com/to/resource" — protocol & hostname lowercased, path normalized
// trailing slash normalization
function normalisasiURL(urlString: string): string {
const url = new URL(urlString);
// remove the trailing slash on the pathname (except when it's just "/")
if (url.pathname !== "/" && url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
// sort query parameters for consistency
url.searchParams.sort();
// remove the fragment (hash) — usually irrelevant for APIs
url.hash = "";
return url.href;
}
console.log(normalisasiURL("https://example.com/path/?b=2&a=1#section"));
// "https://example.com/path?a=1&b=2"
// extract and normalize a domain
function normalisasiDomain(urlString: string): string {
const url = new URL(urlString.startsWith("http") ? urlString : `https://${urlString}`);
return url.hostname.toLowerCase();
}
console.log(normalisasiDomain("HTTPS://WWW.Example.COM/path")); // "www.example.com"
console.log(normalisasiDomain("example.com")); // "example.com"
// check whether two URLs refer to the same resource
function urlSama(a: string, b: string): boolean {
return normalisasiURL(a) === normalisasiURL(b);
}
console.log(urlSama(
"https://example.com/path/?b=2&a=1",
"https://example.com/path?a=1&b=2"
)); // true
Common Patterns in Applications #
URL Builder for an API Client #
class APIClient {
private baseURL: URL;
constructor(baseURL: string, private apiKey?: string) {
this.baseURL = new URL(baseURL);
}
// build an endpoint URL with path and parameters
buildURL(
path: string,
params?: Record<string, string | number | boolean | undefined>
): string {
// combine the base path with the endpoint path
const url = new URL(
path.startsWith("/") ? path : `/${path}`,
this.baseURL
);
// add the API key if present
if (this.apiKey) {
url.searchParams.set("apiKey", this.apiKey);
}
// add the given parameters (skip undefined/null)
if (params) {
for (const [key, value] of Object.entries(params)) {
if (value !== undefined && value !== null) {
url.searchParams.set(key, String(value));
}
}
}
return url.href;
}
// build a URL with a path containing dynamic segments
buildURLWithSegments(
template: string,
segments: Record<string, string>,
params?: Record<string, string | number | undefined>
): string {
// replace :param with encoded values
const path = template.replace(/:(\w+)/g, (_, key) => {
const value = segments[key];
if (value === undefined) throw new Error(`Segment :${key} tidak ditemukan`);
return encodeURIComponent(value);
});
return this.buildURL(path, params);
}
}
// usage
const client = new APIClient("https://api.tokoku.com/v2", "sk_test_1234567890");
console.log(client.buildURL("/produk", {
kategori: "elektronik",
hargaMin: 100000,
hargaMax: 5000000,
halaman: 1,
}));
// "https://api.tokoku.com/v2/produk?apiKey=sk_test_1234567890&kategori=elektronik&hargaMin=100000&..."
console.log(client.buildURLWithSegments(
"/produk/:id/ulasan",
{ id: "prod-abc-123" },
{ halaman: 1, perHalaman: 10 }
));
// "https://api.tokoku.com/v2/produk/prod-abc-123/ulasan?apiKey=...&halaman=1&perHalaman=10"
Parsing Query Parameters from a Request #
// parse the query string from an incoming request URL
function parseQueryParams(urlString: string): Record<string, string | string[]> {
const url = new URL(urlString);
const result: Record<string, string | string[]> = {};
for (const key of new Set(url.searchParams.keys())) {
const values = url.searchParams.getAll(key);
// if only one value, store as a string; more than one as an array
result[key] = values.length === 1 ? values[0] : values;
}
return result;
}
console.log(parseQueryParams(
"https://example.com/cari?q=laptop&tag=gaming&tag=sale&halaman=1"
));
// { q: "laptop", tag: ["gaming", "sale"], halaman: "1" }
// helper: get a value with the correct type
function getQueryParam(url: URL, key: string): string | null;
function getQueryParam(url: URL, key: string, defaultValue: string): string;
function getQueryParam(url: URL, key: string, defaultValue?: string): string | null {
return url.searchParams.get(key) ?? defaultValue ?? null;
}
function getQueryParamInt(url: URL, key: string, defaultValue: number = 0): number {
const val = url.searchParams.get(key);
const parsed = val !== null ? parseInt(val, 10) : NaN;
return isNaN(parsed) ? defaultValue : parsed;
}
function getQueryParamBool(url: URL, key: string, defaultValue: boolean = false): boolean {
const val = url.searchParams.get(key);
if (val === null) return defaultValue;
return val === "true" || val === "1" || val === "yes";
}
// usage in a request handler
function handleProdukRequest(requestURL: string): void {
const url = new URL(requestURL);
const query = getQueryParam(url, "q", "");
const halaman = getQueryParamInt(url, "halaman", 1);
const perHalaman = getQueryParamInt(url, "perHalaman", 20);
const hargaMax = getQueryParamInt(url, "hargaMax", 0);
const stokAda = getQueryParamBool(url, "stokAda", false);
console.log({ query, halaman, perHalaman, hargaMax, stokAda });
}
URL Security — Preventing Open Redirects #
An open redirect happens when an application forwards users to a URL that comes from input without validation — attackers can direct users to a malicious site.
// ANTI-PATTERN: redirecting to an input URL without validation
function redirectSalah(req: any, res: any): void {
const tujuan = req.query.redirect;
res.redirect(tujuan); // ✗ can redirect to any site!
}
// CORRECT: validate the redirect URL only against allowed domains
function isRedirectAman(
urlInput: string,
domainDiizinkan: string[]
): boolean {
try {
const url = new URL(urlInput);
return domainDiizinkan.includes(url.hostname);
} catch {
// invalid URL — maybe a relative path like "/dashboard"
// relative paths are safe because they can't redirect to another domain
return urlInput.startsWith("/") && !urlInput.startsWith("//");
}
}
function getURLRedirectAman(
urlInput: string,
domainDiizinkan: string[],
fallback: string = "/"
): string {
return isRedirectAman(urlInput, domainDiizinkan) ? urlInput : fallback;
}
// usage
const domainSah = ["tokoku.com", "www.tokoku.com", "api.tokoku.com"];
console.log(isRedirectAman("https://tokoku.com/dashboard", domainSah)); // true
console.log(isRedirectAman("https://evil.com/phishing", domainSah)); // false
console.log(isRedirectAman("/dashboard", domainSah)); // true (relative)
console.log(isRedirectAman("//evil.com", domainSah)); // false (protocol-relative)
// in an Express middleware
function redirectMiddleware(req: any, res: any): void {
const tujuanRaw = req.query.redirect || "/";
const tujuanAman = getURLRedirectAman(tujuanRaw, domainSah);
res.redirect(302, tujuanAman);
}
Extracting Information from URLs #
// extract all path segments
function getPathSegments(urlString: string): string[] {
const url = new URL(urlString);
return url.pathname
.split("/")
.filter((segment) => segment.length > 0); // remove empty strings
}
console.log(getPathSegments("https://example.com/v1/produk/laptop-asus-123"));
// ["v1", "produk", "laptop-asus-123"]
// extract the file extension from a URL
function getFileExtension(urlString: string): string {
const url = new URL(urlString);
const filename = url.pathname.split("/").pop() ?? "";
const dotIndex = filename.lastIndexOf(".");
return dotIndex > 0 ? filename.slice(dotIndex) : "";
}
console.log(getFileExtension("https://cdn.example.com/images/foto.jpg")); // ".jpg"
console.log(getFileExtension("https://example.com/api/v1/data")); // ""
// check whether a URL points to a specific resource
function isAPIEndpoint(urlString: string, apiBasePath: string = "/api"): boolean {
try {
const url = new URL(urlString);
return url.pathname.startsWith(apiBasePath);
} catch {
return false;
}
}
// parse a URL into a more structured object
interface ParsedURL {
protocol: string;
hostname: string;
port: string | null;
path: string;
segments: string[];
query: Record<string, string | string[]>;
hash: string | null;
}
function parseURL(urlString: string): ParsedURL {
const url = new URL(urlString);
const query: Record<string, string | string[]> = {};
for (const key of new Set(url.searchParams.keys())) {
const values = url.searchParams.getAll(key);
query[key] = values.length === 1 ? values[0] : values;
}
return {
protocol: url.protocol.replace(":", ""),
hostname: url.hostname,
port: url.port || null,
path: url.pathname,
segments: url.pathname.split("/").filter(Boolean),
query,
hash: url.hash ? url.hash.slice(1) : null, // remove the leading '#'
};
}
console.log(parseURL("https://api.example.com:8080/v1/produk?q=laptop&tag=gaming#atas"));
// {
// protocol: "https",
// hostname: "api.example.com",
// port: "8080",
// path: "/v1/produk",
// segments: ["v1", "produk"],
// query: { q: "laptop", tag: "gaming" },
// hash: "atas"
// }
When to Use URL vs Plain Strings #
Use the URL API for:
✓ Parsing incoming request URLs — get the path, query, hostname
✓ Building URLs with query parameters — URLSearchParams handles encoding automatically
✓ Modifying URL components — change the hostname, add parameters, change the path
✓ Normalizing URLs for comparison or cache keys
✓ Validating whether a string is a valid URL
✓ Combining a base URL with a relative path
Plain strings are fine for:
✗ URLs whose format is fixed and don't need modification
✗ Simple URL templates without special characters in parameters
✗ Logging already-formed URLs — no parsing needed
Summary #
- The
URLAPI is the WHATWG standard — runs identically in Node.js and browsers; use it instead of manual string manipulation for all URL operations.URLSearchParamsfor query strings — handles encoding/decoding of special characters automatically; don’t concatenate query parameters manually because it’s prone to encoding bugs.url.searchParams.set()not concatenation — when adding parameters to an existing URL, always usesearchParams.set()so encoding happens correctly.encodeURIComponentfor values, not full URLs — use it to encode query parameter values or individual path segments; letURLSearchParamshandle the overall encoding.URL()throws aTypeErrorfor invalid URLs — take advantage of this for URL validation with try/catch; no complicated regex needed.- Normalize URLs before comparing them — the same URL can be written differently (case, trailing slash, parameter order); normalize first for accurate comparison.
- Validate redirect URLs against a domain list — don’t redirect to a raw URL from user input; always check the hostname against a whitelist to prevent open redirect attacks.
- Relative paths starting with
/are safe for redirects — only absolute URLs can point to another domain; relative paths always stay within the same domain.