Server hanyalah separuh cerita: hampir semua aplikasi Node.js juga perlu menjadi klien — memanggil API cuaca, mengirim notifikasi ke layanan lain, atau mengunduh berkas dari CDN. Untuk tugas ini Node.js menyediakan fetch sebagai global function, API yang sama persis dengan yang kita kenal di browser sehingga keahlian satu sisi langsung terpakai di sisi lain. Pada chapter ini kita konsumsi API lengkap dari sisi klien: membaca response, mengirim JSON, memeriksa kegagalan, dan membatalkan permintaan yang terlalu lama.

fetch Global di Balik Layar #

Tidak perlu import apa pun: sejak Node.js 18, fetch tersedia langsung di setiap berkas, terdokumentasi di referensi globals Node.js . Implementasinya ditopang package undici — HTTP client buatan tim Node.js yang menempel dekat dengan jaringan — namun kita tidak perlu memikirkan lapisan itu; dari kursi penulis aplikasi, fetch di Node.js dan di browser berperilaku sama. Panggilan paling sederhana hanya butuh satu argumen URL dan mengembalikan object response:

js
const r = await fetch(url); // url berisi alamat API yang dituju
r.status;   // 200
r.ok;       // true bila status berada di rentang 200-299
await r.json(); // body diurai menjadi object JavaScript

Dua hal perlu dicermati dari cuplikan di atas. Pertama, fetch mengembalikan Promise, jadi await adalah bentuk penulisan yang wajar. Kedua, body response adalah stream — karenanya r.json() dan r.text() juga mengembalikan Promise, dan body hanya bisa dibaca satu kali; bila kita membutuhkan isinya di dua tempat, baca sekali lalu simpan hasilnya ke variable.

Percakapan Lengkap: GET, POST, PUT, DELETE #

Demo berikut memakai server lokal kecil sebagai lawan bicara — pola uji mandiri yang sama dengan chapter-chapter sebelumnya — lalu menjalankan lima operasi khas REST dari sisi klien:

js
import http from 'node:http';

const host = '127.0.0.1';
const tugas = [
  { id: 1, judul: 'Belajar HTTP' },
  { id: 2, judul: 'Coba fetch' },
];

const server = http.createServer((req, res) => {
  const url = new URL(req.url, `http://${host}`);
  const json = (status, data) => {
    res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
    res.end(JSON.stringify(data));
  };

  if (req.method === 'GET' && url.pathname === '/tugas') return json(200, tugas);

  const cocok = url.pathname.match(/^\/tugas\/(\d+)$/);
  if (req.method === 'GET' && cocok) {
    const item = tugas.find((t) => t.id === Number(cocok[1]));
    return item ? json(200, item) : json(404, { galat: 'tugas tidak ada' });
  }
  if (req.method === 'POST' && url.pathname === '/tugas') {
    return json(201, { id: 3, judul: 'tugas baru' });
  }
  if (req.method === 'PUT' && cocok) {
    return json(200, { id: Number(cocok[1]), judul: 'judul baru' });
  }
  if (req.method === 'DELETE' && cocok) {
    return json(200, { terhapus: Number(cocok[1]) });
  }
  return json(404, { galat: 'jalur tidak dikenal' });
});

server.listen(0, '127.0.0.1', async () => {
  const port = server.address().port;
  const dasar = `http://127.0.0.1:${port}`;

  const r1 = await fetch(dasar + '/tugas/2');
  console.log('GET     :', r1.status, r1.ok, JSON.stringify(await r1.json()));

  const r2 = await fetch(dasar + '/tugas/99');
  console.log('GET     :', r2.status, r2.ok, JSON.stringify(await r2.json()));

  const r3 = await fetch(dasar + '/tugas', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ judul: 'tugas dari klien' }),
  });
  console.log('POST    :', r3.status, r3.ok, JSON.stringify(await r3.json()));

  const r4 = await fetch(dasar + '/tugas/2', {
    method: 'PUT',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ judul: 'judul diubah' }),
  });
  console.log('PUT     :', r4.status, JSON.stringify(await r4.json()));

  const r5 = await fetch(dasar + '/tugas/2', { method: 'DELETE' });
  console.log('DELETE  :', r5.status, JSON.stringify(await r5.json()));

  const pengendali = new AbortController();
  pengendali.abort();
  try {
    await fetch(dasar + '/tugas', { signal: pengendali.signal });
  } catch (e) {
    console.log('abort   :', e.name, '-', e.message);
  }

  server.close();

  // port 47777 sengaja dipilih karena tidak ada yang mendengarkan di sana
  try {
    await fetch(`http://127.0.0.1:47777/tugas`);
  } catch (e) {
    console.log('koneksi :', e.name, '-', e.cause?.code);
  }
});

Output yang Dihasilkan:

text
GET     : 200 true {"id":2,"judul":"Coba fetch"}
GET     : 404 false {"galat":"tugas tidak ada"}
POST    : 201 true {"id":3,"judul":"tugas baru"}
PUT     : 200 {"id":2,"judul":"judul baru"}
DELETE  : 200 {"terhapus":2}
abort   : AbortError - This operation was aborted
koneksi : TypeError - ECONNREFUSED

Mari kita telaah bagian-bagian klien pada berkas tersebut:

  1. Baris 39-43 (dua GET): respons sukses dan gagal diperlakukan sama — keduanya menghasilkan object response. Pada baris kedua, status 404 tercetak bersama r2.ok bernilai false, dan body JSON galat tetap terbaca. Tidak ada exception di sini, sebab server menjawab dengan sah.
  2. Baris 45-50 (POST): method ditentukan lewat opsi method, body dikirim lewat opsi body berupa hasil JSON.stringify, dan header Content-Type: application/json dinyatakan lewat object headers — pola yang sama berlaku untuk PUT pada baris 52-57.
  3. Baris 59-60 (DELETE): operasi hapus tidak membutuhkan body; cukup method dan URL.
  4. Baris 62-68 (abort): AbortController dibuat, langsung dihentikan lewat abort(), lalu signal-nya diteruskan ke fetch. Permintaan yang dibatalkan melempar exception bernama AbortError — tertangkap try...catch lalu dicetak namanya dan pesannya.
  5. Baris 72-77 (koneksi gagal): setelah server ditutup, fetch ke port yang sengaja kosong dijawab sistem operasi dengan penolakan koneksi. Exception TypeError bermuatan cause berisi kode ECONNREFUSED — penyebab tingkat jaringan yang sesungguhnya.

Membatalkan Permintaan dengan AbortController #

Pada demo, abort() dipanggil seketika sehingga pembatalannya pasti terjadi — bentuk yang paling mudah dibuktikan. Pemakaian yang paling umum adalah timeout: kita membatalkan permintaan bila server terlalu lama menjawab. Polanya menggabungkan timer dan signal:

js Pola timeout untuk fetch
const pengendali = new AbortController();
const batasWaktu = setTimeout(() => pengendali.abort(), 5000);

try {
  const r = await fetch(url, { signal: pengendali.signal });
  if (!r.ok) throw new Error(`server menjawab ${r.status}`);
  const data = await r.json();
} finally {
  clearTimeout(batasWaktu);
}

Timer lima detik memanggil abort() bila response belum tiba, dan clearTimeout di blok finally memastikan timer tidak menyala setelah permintaan selesai. Tanpa pola seperti ini, satu server yang lambat bisa menahan proses kita menunggu berlama-lama — pola yang kita kenal sebagai sumber kebuntuan pada Section Asynchronous Runtime.

Tiga Wajah Kegagalan #

Output demo memperlihatkan ketiga wajah kegagalan yang perlu kita bedakan setiap menulis klien HTTP:

WajahTandaPenanganan
Galat jaringanfetch melempar TypeError, cause.code seperti ECONNREFUSEDtry...catch di sekitar pemanggilan
Dibatalkanexception AbortErrortry...catch; periksa e.name
Galat HTTPresponse biasa dengan r.ok === falseperiksa r.ok atau r.status

Dokumentasi lengkap seluruh opsi fetch — mulai dari header khusus, mode cache, hingga signal — terdokumentasi di MDN tentang fetch , sedangkan AbortController dibahas tuntas di MDN tentang AbortController .

Kita kini menguasai dua arah percakapan HTTP. Satu tantangan klasik masih tersisa untuk menutup Section ini: melayani berkas sungguhan — HTML, CSS, gambar — dengan aman, lengkap dengan penanganan jalur yang mencoba keluar dari folder yang kita izinkan. Chapter Static File Server merangkai module http, fs, dan path untuk tugas tersebut.