URL #

URL (Uniform Resource Locator) adalah antarmuka fundamental web — setiap request HTTP, setiap link, setiap endpoint API direpresentasikan sebagai URL. Manipulasi URL yang benar bukan sekadar konkatenasi string — ada encoding karakter khusus, normalisasi path, penanganan query parameter, dan banyak edge case yang mudah salah jika dikerjakan manual. Node.js menyediakan URL dan URLSearchParams sebagai API bawaan yang mengikuti standar WHATWG URL — sama persis dengan yang digunakan di browser, sehingga kode yang kamu tulis di server bisa berjalan di browser tanpa modifikasi.

Anatomi URL #

Sebelum menulis kode, penting memahami komponen-komponen yang membentuk sebuah 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);      // URL lengkap sebagai string

Membuat dan Parsing URL #

Konstruktor URL #

URL menerima string URL absolut, atau URL relatif dengan base URL sebagai argumen kedua.

// URL absolut
const url1 = new URL("https://example.com/path?q=hello");

// URL relatif terhadap 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: konkatenasi string untuk membangun URL
function getEndpointSalah(baseUrl: string, path: string, id: string): string {
  return baseUrl + "/" + path + "/" + id; // ✗ tidak aman, tidak normalisasi
}

// BENAR: gunakan URL API
function getEndpoint(baseUrl: string, path: string, id: string): string {
  return new URL(`${path}/${id}`, baseUrl).href; // ✓ normalisasi otomatis
}

// validasi URL — URL() melempar TypeError jika tidak valid
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 — URL relatif tanpa base

// validasi hanya http/https
function isHTTPUrl(urlString: string): boolean {
  try {
    const url = new URL(urlString);
    return url.protocol === "http:" || url.protocol === "https:";
  } catch {
    return false;
  }
}

Memodifikasi Komponen URL #

Properti URL bisa di-set langsung — URL otomatis di-update dan dinormalisasi.

const url = new URL("https://example.com/path");

// ubah protokol
url.protocol = "http:";
console.log(url.href); // "http://example.com/path"

// ubah hostname
url.hostname = "api.example.com";
console.log(url.href); // "http://api.example.com/path"

// ubah port
url.port = "3000";
console.log(url.href); // "http://api.example.com:3000/path"

// tambah path
url.pathname = "/v2/users";
console.log(url.href); // "http://api.example.com:3000/v2/users"

// hapus port — set ke string kosong
url.port = "";
console.log(url.href); // "http://api.example.com/v2/users"

// tambah hash
url.hash = "section-1";
console.log(url.href); // "http://api.example.com/v2/users#section-1"

URLSearchParams — Query String #

URLSearchParams adalah API khusus untuk mengelola query parameter. Ia menangani encoding/decoding karakter khusus secara otomatis.

// buat dari string query
const params1 = new URLSearchParams("q=laptop+gaming&kategori=elektronik&halaman=1");

// buat dari object
const params2 = new URLSearchParams({
  q: "laptop gaming",
  kategori: "elektronik",
  halaman: "1",
});

// buat dari array of pairs
const params3 = new URLSearchParams([
  ["q", "laptop gaming"],
  ["tag", "sale"],
  ["tag", "new"], // key duplikat diizinkan
]);

Operasi pada Query Parameter #

const params = new URLSearchParams();

// set — mengganti nilai jika key sudah ada
params.set("q", "laptop");
params.set("halaman", "1");
params.set("perHalaman", "20");

// append — menambah nilai baru meski key sudah ada
params.append("tag", "gaming");
params.append("tag", "sale");     // dua nilai untuk key "tag"

// get — ambil nilai pertama dari key
console.log(params.get("q"));       // "laptop"
console.log(params.get("tag"));     // "gaming" — hanya yang pertama
console.log(params.get("kosong"));  // null — key tidak ada

// getAll — ambil semua nilai dari key
console.log(params.getAll("tag")); // ["gaming", "sale"]

// has — cek apakah key ada
console.log(params.has("q"));       // true
console.log(params.has("kosong"));  // false

// delete — hapus semua nilai dari key
params.delete("tag");
console.log(params.has("tag")); // false

// konversi ke string
console.log(params.toString());
// "q=laptop&halaman=1&perHalaman=20"

// iterasi semua parameter
for (const [key, value] of params) {
  console.log(`${key}: ${value}`);
}

// atau dengan forEach
params.forEach((value, key) => {
  console.log(`${key} = ${value}`);
});

// sort — urutkan parameter secara alfabetis (berguna untuk cache key yang konsisten)
params.sort();
console.log(params.toString());
// "halaman=1&perHalaman=20&q=laptop"

Integrasi URLSearchParams dengan URL #

const url = new URL("https://api.example.com/produk");

// akses searchParams dari 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"

// modifikasi langsung via searchParams
url.searchParams.delete("tag");
url.searchParams.set("halaman", "2");

console.log(url.search); // "?q=laptop&kategori=elektronik&halaman=2"

// baca query parameter dari URL yang sudah ada
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

Encoding URL #

URL hanya boleh mengandung karakter ASCII tertentu. Karakter lain — spasi, karakter non-ASCII, karakter khusus — harus di-encode dalam format %XX.

flowchart TD
    A[Karakter dalam URL] --> B{Karakter aman\nuntuk URL?}
    B -- Ya --> C["Tulis apa adanya\nA-Z, a-z, 0-9, - _ . ~"]
    B -- Tidak --> D["Encode: %XX\nSpasi → %20\nä → %C3%A4"]
    D --> E[URL yang valid]
    C --> E

encodeURIComponent vs encodeURI #

// encodeURIComponent — encode semua karakter kecuali: A-Z a-z 0-9 - _ . ! ~ * ' ( )
// gunakan untuk nilai query parameter atau path segment
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 — encode URL lengkap, tapi TIDAK encode karakter struktural URL
// : / ? # [ ] @ ! $ & ' ( ) * + , ; =
// gunakan untuk encode URL yang sudah terbentuk (jarang dibutuhkan)
console.log(encodeURI("https://example.com/path dengan spasi?q=nilai"));
// "https://example.com/path%20dengan%20spasi?q=nilai"
// perhatikan: ? dan = tidak di-encode karena merupakan bagian struktur URL

// ANTI-PATTERN: encode query parameter dengan encodeURI
function buatURLSalah(base: string, keyword: string): string {
  return encodeURI(`${base}?q=${keyword}`); // ✗ tidak meng-encode karakter & = di keyword
}

// BENAR: gunakan URLSearchParams yang otomatis encode
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); // ✓ encode otomatis
  }
  return url.href;
}

console.log(buatURL("https://example.com/cari", {
  q: "laptop & tablet",    // & di dalam nilai — di-encode otomatis
  kategori: "elektronik=premium", // = di dalam nilai — di-encode otomatis
}));
// "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"

Normalisasi URL #

URL yang berbeda secara tekstual bisa merujuk ke resource yang sama. Normalisasi memastikan representasi yang konsisten.

// URL() sudah melakukan normalisasi dasar
const url1 = new URL("HTTPS://Example.COM/PATH/../to/./resource");
console.log(url1.href);
// "https://example.com/to/resource" — protokol & hostname jadi lowercase, path dinormalisasi

// normalisasi trailing slash
function normalisasiURL(urlString: string): string {
  const url = new URL(urlString);

  // hapus trailing slash pada pathname (kecuali hanya "/")
  if (url.pathname !== "/" && url.pathname.endsWith("/")) {
    url.pathname = url.pathname.slice(0, -1);
  }

  // urutkan query parameter untuk konsistensi
  url.searchParams.sort();

  // hapus fragment (hash) — biasanya tidak relevan untuk API
  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"

// ekstrak dan normalisasi 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"

// cek apakah dua URL merujuk ke resource yang sama
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

Pola Umum di Aplikasi #

URL Builder untuk API Client #

class APIClient {
  private baseURL: URL;

  constructor(baseURL: string, private apiKey?: string) {
    this.baseURL = new URL(baseURL);
  }

  // bangun URL endpoint dengan path dan parameter
  buildURL(
    path: string,
    params?: Record<string, string | number | boolean | undefined>
  ): string {
    // gabungkan base path dengan endpoint path
    const url = new URL(
      path.startsWith("/") ? path : `/${path}`,
      this.baseURL
    );

    // tambah API key jika ada
    if (this.apiKey) {
      url.searchParams.set("apiKey", this.apiKey);
    }

    // tambah parameter yang diberikan (skip yang 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;
  }

  // bangun URL dengan path yang mengandung dynamic segment
  buildURLWithSegments(
    template: string,
    segments: Record<string, string>,
    params?: Record<string, string | number | undefined>
  ): string {
    // ganti :param dengan nilai yang di-encode
    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);
  }
}

// penggunaan
const client = new APIClient("https://api.tokoku.com/v2", "sk_prod_abc123");

console.log(client.buildURL("/produk", {
  kategori: "elektronik",
  hargaMin: 100000,
  hargaMax: 5000000,
  halaman: 1,
}));
// "https://api.tokoku.com/v2/produk?apiKey=sk_prod_abc123&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 Parameter dari Request #

// parse query string dari URL request yang masuk
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);
    // jika hanya satu nilai, simpan sebagai string; lebih dari satu sebagai 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: ambil nilai dengan tipe yang benar
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";
}

// penggunaan di handler request
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 });
}

Keamanan URL — Mencegah Open Redirect #

Open redirect terjadi saat aplikasi meneruskan pengguna ke URL yang berasal dari input tanpa validasi — penyerang bisa mengarahkan pengguna ke situs berbahaya.

// ANTI-PATTERN: redirect ke URL dari input tanpa validasi
function redirectSalah(req: any, res: any): void {
  const tujuan = req.query.redirect;
  res.redirect(tujuan); // ✗ bisa redirect ke situs manapun!
}

// BENAR: validasi URL redirect hanya ke domain yang diizinkan
function isRedirectAman(
  urlInput: string,
  domainDiizinkan: string[]
): boolean {
  try {
    const url = new URL(urlInput);
    return domainDiizinkan.includes(url.hostname);
  } catch {
    // URL tidak valid — mungkin path relatif seperti "/dashboard"
    // path relatif aman karena tidak bisa redirect ke domain lain
    return urlInput.startsWith("/") && !urlInput.startsWith("//");
  }
}

function getURLRedirectAman(
  urlInput: string,
  domainDiizinkan: string[],
  fallback: string = "/"
): string {
  return isRedirectAman(urlInput, domainDiizinkan) ? urlInput : fallback;
}

// penggunaan
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 (relatif)
console.log(isRedirectAman("//evil.com", domainSah));                    // false (protocol-relative)

// di middleware Express
function redirectMiddleware(req: any, res: any): void {
  const tujuanRaw = req.query.redirect || "/";
  const tujuanAman = getURLRedirectAman(tujuanRaw, domainSah);
  res.redirect(302, tujuanAman);
}

Ekstrak Informasi dari URL #

// ekstrak semua path segment
function getPathSegments(urlString: string): string[] {
  const url = new URL(urlString);
  return url.pathname
    .split("/")
    .filter((segment) => segment.length > 0); // hapus string kosong
}

console.log(getPathSegments("https://example.com/v1/produk/laptop-asus-123"));
// ["v1", "produk", "laptop-asus-123"]

// ekstrak extension file dari 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"));          // ""

// cek apakah URL mengarah ke resource tertentu
function isAPIEndpoint(urlString: string, apiBasePath: string = "/api"): boolean {
  try {
    const url = new URL(urlString);
    return url.pathname.startsWith(apiBasePath);
  } catch {
    return false;
  }
}

// parse URL ke objek yang lebih terstruktur
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, // hapus '#' di depan
  };
}

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"
// }

Kapan Menggunakan URL vs String Biasa #

Gunakan URL API untuk:
  ✓ Parsing URL dari request yang masuk — ambil path, query, hostname
  ✓ Membangun URL dengan query parameter — URLSearchParams handle encoding otomatis
  ✓ Modifikasi komponen URL — ganti hostname, tambah parameter, ubah path
  ✓ Normalisasi URL untuk perbandingan atau cache key
  ✓ Validasi apakah string adalah URL yang valid
  ✓ Menggabungkan base URL dengan relative path

Cukup dengan string biasa untuk:
  ✗ URL yang sudah pasti formatnya dan tidak perlu dimodifikasi
  ✗ Template URL sederhana tanpa karakter khusus di parameter
  ✗ Logging URL yang sudah terbentuk — tidak perlu parsing

Ringkasan #

  • URL API adalah standar WHATWG — berjalan identik di Node.js dan browser; gunakan ini daripada manipulasi string manual untuk semua operasi URL.
  • URLSearchParams untuk query string — menangani encoding/decoding karakter khusus secara otomatis; jangan konkatenasi query parameter secara manual karena rawan bug encoding.
  • url.searchParams.set() bukan konkatenasi — saat menambah parameter ke URL yang sudah ada, selalu gunakan searchParams.set() agar encoding terjadi dengan benar.
  • encodeURIComponent untuk nilai, bukan URL lengkap — gunakan untuk encode nilai query parameter atau path segment individual; biarkan URLSearchParams yang menangani encoding keseluruhan.
  • URL() melempar TypeError untuk URL tidak valid — manfaatkan ini untuk validasi URL dengan try/catch; tidak perlu regex rumit.
  • Normalisasi URL sebelum dibandingkan — URL yang sama bisa ditulis berbeda (case, trailing slash, urutan parameter); normalisasi dulu untuk perbandingan yang akurat.
  • Validasi URL redirect terhadap daftar domain — jangan redirect ke URL mentah dari input pengguna; selalu cek hostname terhadap whitelist untuk mencegah open redirect attack.
  • Path relatif yang diawali / aman untuk redirect — hanya URL absolut yang bisa mengarah ke domain lain; path relatif selalu dalam domain yang sama.

← Sebelumnya: Crypto   Berikutnya: Buffer →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact