Kodeloka
  1. Kodeloka
  2. Kelas JavaScript
  3. Materi

3 · Asinkron & Iterasi · Modul 12 dari 22

Fetch API, Pengolahan JSON, URLSearchParams, dan AbortController

Mengambil dan mengirim data melalui HTTP menggunakan fetch(), memahami mengapa response.ok wajib diperiksa secara eksplisit, menyusun parameter URL dengan URLSearchParams, serta membatalkan permintaan lambat dengan AbortController dan AbortSignal.timeout().

1. Cara Kerja fetch() dan Mengapa Butuh Dua Kali await

Fetch API adalah standar modern di peramban maupun runtime server (Node.js 18+) untuk melakukan permintaan jaringan HTTP berbasis Promise.

Hal pertama yang sering membingungkan pemula adalah mengapa mengambil data dengan fetch() membutuhkan dua kali await:

async function ambilDaftarKelas() {
  // await pertama: Menunggu HEADER respons HTTP tiba dari server (status 200, Content-Type, dsb.)
  const respons = await fetch("https://kodeloka.asadinedu.com/data/catalog.json");

  // Wajib periksa apakah kode status berada di rentang sukses 200–299!
  if (!respons.ok) {
    throw new Error(`Permintaan gagal dengan status HTTP ${respons.status}`);
  }

  // await kedua: Menunggu aliran isi BODY selesai diunduh dan diurai sebagai objek JSON
  const data = await respons.json();
  return data;
}

Jebakan Paling Umum: fetch() TIDAK Melempar Galat pada Status 404 atau 500!

Berbeda dengan beberapa pustaka pihak ketiga, fetch() hanya akan menolak (reject) Promise-nya apabila terjadi kegagalan jaringan tingkat bawah (misalnya perangkat pengguna terputus dari internet, resolusi DNS gagal, atau permintaan dibatalkan).

Jika server membalas dengan kode status 404 Not Found, 401 Unauthorized, atau 500 Internal Server Error, fetch() menganggap komunikasi jaringan berhasil dan **tetap me-resolve Promise menjadi objek Response (dengan respons.ok bernilai false)! Oleh karena itu, selalu tulis pemeriksaan if (!respons.ok)** sebelum memanggil await respons.json().

2. Mengirim Data dengan POST, Header, dan FormData

A. Mengirim Payload JSON (Content-Type: application/json)

Ketika mengirim objek JavaScript sebagai JSON melalui metode POST, PUT, atau PATCH, Anda harus mengubah objek menjadi string menggunakan JSON.stringify() dan mencantumkan header Content-Type: application/json:

async function simpanProgresModul(idModul, skorKuis) {
  const respons = await fetch("/api/progres", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json"
    },
    body: JSON.stringify({
      modul_id: idModul,
      skor: skorKuis,
      waktu: new Date().toISOString()
    })
  });

  if (!respons.ok) {
    const rincianGalat = await respons.json().catch(() => null);
    throw new Error(rincianGalat?.pesan ?? `Gagal menyimpan (HTTP ${respons.status})`);
  }

  return await respons.json();
}

B. Mengirim Formulir atau Berkas dengan FormData

Jika Anda mengambil data langsung dari elemen <form> di HTML (termasuk unggahan berkas <input type="file">), gunakan objek FormData:

async function kirimFormulirPendaftaran(elemenForm) {
  const dataForm = new FormData(elemenForm);

  const respons = await fetch("/api/pendaftaran", {
    method: "POST",
    // PENTING: JANGAN mengatur header Content-Type secara manual saat mengirim FormData!
    // Peramban otomatis mengatur Content-Type: multipart/form-data beserta string 'boundary' uniknya.
    body: dataForm
  });

  if (!respons.ok) throw new Error(`Gagal mengirim formulir: ${respons.status}`);
  return await respons.json();
}

3. Merangkai Parameter Query Secara Aman dengan URL dan URLSearchParams

Jangan pernah menyambung parameter URL secara manual dengan string template seperti ` /api/cari?q=${kataKunci} ! Jika pengguna mengetik kata kunci "C++ & Go", karakter & dan +` akan merusak struktur parameter URL Anda.

Gunakan antarmuka standar URL dan URLSearchParams yang otomatis mengodekan karakter khusus (URL-encoding) dengan aman:

function buatUrlPencarian(kataKunci, bidang = "semua", halaman = 1) {
  const url = new URL("https://kodeloka.asadinedu.com/api/kelas");
  url.searchParams.set("q", kataKunci);
  url.searchParams.set("bidang", bidang);
  url.searchParams.set("page", String(halaman));

  return url.toString();
}

console.log(buatUrlPencarian("Git & CI/CD", "infra", 1));
// Keluaran aman: "https://kodeloka.asadinedu.com/api/kelas?q=Git+%26+CI%2FCD&bidang=infra&page=1"

4. Membatalkan Permintaan dan Mengatur Batas Waktu (AbortController & AbortSignal.timeout)

Tanpa batas waktu (timeout), permintaan fetch() ke server yang sedang macet dapat menggantung selama lebih dari satu menit. Selain itu, pada fitur pencarian langsung (live search), ketika pengguna mengetik huruf baru, permintaan pencarian dari huruf sebelumnya harus dibatalkan agar balasannya yang terlambat tidak menimpa hasil terbaru (race condition).

A. Batas Waktu Otomatis Satu Baris dengan AbortSignal.timeout()

Sejak April 2024 (status Baseline Newly Available), Anda tidak perlu lagi membuat setTimeout manual untuk membatalkan fetch(). Cukup pasang signal: AbortSignal.timeout(milidetik):

async function ambilDataDenganBatasWaktu(url, batasMs = 5000) {
  try {
    const respons = await fetch(url, {
      signal: AbortSignal.timeout(batasMs)
    });
    if (!respons.ok) throw new Error(`Status HTTP: ${respons.status}`);
    return await respons.json();
  } catch (err) {
    if (err.name === "TimeoutError") {
      throw new Error(`Permintaan ke server melebihi batas waktu ${batasMs} ms.`);
    }
    if (err.name === "AbortError") {
      throw new Error("Permintaan dibatalkan oleh pengguna.");
    }
    throw err;
  }
}

B. Pembatalan Manual untuk Live Search dengan AbortController

let pengendaliAktif = null;

async function cariMateriLangsung(kataKunci) {
  // Jika masih ada permintaan sebelumnya yang belum selesai, batalkan sekarang!
  if (pengendaliAktif) {
    pengendaliAktif.abort();
  }

  pengendaliAktif = new AbortController();

  const url = new URL("/api/materi", window.location.origin);
  url.searchParams.set("q", kataKunci);

  const respons = await fetch(url, {
    signal: pengendaliAktif.signal
  });
  if (!respons.ok) throw new Error(`HTTP ${respons.status}`);
  return await respons.json();
}

Latihan Praktik: Klien HTTP Tangguh dengan URLSearchParams dan Timeout

Buatlah berkas klien-api.js yang mengekspor fungsi ambilModul(filterObjek, timeoutMs = 4000):

  1. Gunakan new URLSearchParams(filterObjek) untuk merangkai parameter query dengan aman.
  2. Panggil fetch() dengan signal: AbortSignal.timeout(timeoutMs) dan header Accept: application/json.
  3. Periksa respons.ok dan tangani pembedaan antara TimeoutError dan galat HTTP biasa secara rapi.