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:
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:
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:
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 - ECONNREFUSEDMari kita telaah bagian-bagian klien pada berkas tersebut:
- Baris 39-43 (dua GET): respons sukses dan gagal diperlakukan sama — keduanya menghasilkan object response. Pada baris kedua, status
404tercetak bersamar2.okbernilaifalse, dan body JSON galat tetap terbaca. Tidak ada exception di sini, sebab server menjawab dengan sah. - Baris 45-50 (POST): method ditentukan lewat opsi
method, body dikirim lewat opsibodyberupa hasilJSON.stringify, dan headerContent-Type: application/jsondinyatakan lewat objectheaders— pola yang sama berlaku untukPUTpada baris 52-57. - Baris 59-60 (DELETE): operasi hapus tidak membutuhkan body; cukup method dan URL.
- Baris 62-68 (abort):
AbortControllerdibuat, langsung dihentikan lewatabort(), lalu signal-nya diteruskan kefetch. Permintaan yang dibatalkan melempar exception bernamaAbortError— tertangkaptry...catchlalu dicetak namanya dan pesannya. - Baris 72-77 (koneksi gagal): setelah server ditutup,
fetchke port yang sengaja kosong dijawab sistem operasi dengan penolakan koneksi. ExceptionTypeErrorbermuatancauseberisi kodeECONNREFUSED— 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:
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:
| Wajah | Tanda | Penanganan |
|---|---|---|
| Galat jaringan | fetch melempar TypeError, cause.code seperti ECONNREFUSED | try...catch di sekitar pemanggilan |
| Dibatalkan | exception AbortError | try...catch; periksa e.name |
| Galat HTTP | response biasa dengan r.ok === false | periksa 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.