3420 kata
17 menit

Menghubungkan Frontend ke Backend dengan Fetch API

Halo, Gabuters!

Pada artikel sebelumnya, kita berhasil membuat REST API CRUD menggunakan JavaScript tanpa framework.

API tersebut sudah mendukung GET, POST, PUT, dan DELETE. Kita juga belajar membaca request body, memvalidasi data, dan mengirim response JSON.

Namun, selama ini kita menguji API menggunakan browser dan curl.

Bagaimana jika kita ingin menampilkan daftar artikel langsung di halaman HTML?

Bagaimana jika pengunjung bisa menambahkan artikel melalui formulir?

Dan bagaimana cara menampilkan pesan ketika server sedang bermasalah?

Untuk menjawabnya, kita akan belajar Fetch API, yaitu antarmuka JavaScript yang dapat digunakan untuk melakukan HTTP request dari browser.

Pada akhir artikel, kita akan membuat halaman sederhana yang dapat:

  • Mengambil daftar artikel dari backend.
  • Menampilkan data artikel.
  • Menambahkan artikel melalui formulir.
  • Menampilkan indikator loading.
  • Menangani error jaringan dan respons HTTP.
  • Memperbarui tampilan tanpa memuat ulang seluruh halaman.

Kita tetap menggunakan JavaScript murni, tanpa framework frontend maupun backend.

1. Mengapa Frontend Perlu Berkomunikasi dengan Backend?#

Bayangkan kita mempunyai halaman daftar artikel.

Jika seluruh artikel ditulis langsung dalam HTML, setiap perubahan data mungkin mengharuskan kita mengubah kode halaman.

Namun, jika artikel disediakan melalui API, frontend dapat meminta data terbaru dari backend.

Alurnya seperti ini:

Pengguna membuka halaman
|
v
JavaScript menjalankan fetch()
|
| HTTP GET
v
Backend API
|
| Response JSON
v
JavaScript membaca response
|
v
Halaman menampilkan artikel

Dengan cara ini, frontend bertanggung jawab menampilkan data, sedangkan backend bertanggung jawab menyediakan dan memprosesnya.

Pemisahan tersebut memudahkan kita memahami peran masing-masing komponen.

2. Apa Itu Fetch API?#

Fetch API adalah antarmuka yang disediakan lingkungan seperti browser untuk melakukan request dan menerima response.

Di browser modern, kita dapat menggunakannya melalui function fetch().

Contoh sederhana:

fetch("/api/artikel");

Kode tersebut mengirim request ke endpoint /api/artikel pada origin yang sama dengan halaman.

Namun, fetch() tidak langsung menghasilkan data artikel.

Function tersebut mengembalikan Promise.

Promise merepresentasikan hasil operasi asynchronous yang mungkin tersedia kemudian.

Karena komunikasi jaringan membutuhkan waktu, JavaScript tidak harus menghentikan seluruh interaksi halaman sambil menunggu respons.

3. Mengenal Promise#

Sebelum menggunakan Fetch API lebih jauh, kita perlu memahami Promise.

Promise mempunyai tiga keadaan utama:

  • pending: operasi masih berlangsung.
  • fulfilled: operasi berhasil diselesaikan.
  • rejected: operasi gagal.

Contohnya:

const janji = new Promise((resolve) => {
setTimeout(() => {
resolve("Data berhasil diperoleh.");
}, 1000);
});
janji.then((hasil) => {
console.log(hasil);
});

Setelah sekitar satu detik, console menampilkan:

Data berhasil diperoleh.

Pada contoh tersebut, resolve() menyelesaikan Promise dengan hasil tertentu.

Kita juga dapat menangani penolakan Promise menggunakan .catch().

Contohnya:

const janji = Promise.reject(
new Error("Terjadi kesalahan.")
);
janji
.then((hasil) => {
console.log(hasil);
})
.catch((error) => {
console.error(error.message);
});

Hasilnya:

Terjadi kesalahan.

Promise sangat sering digunakan ketika bekerja dengan API.

4. Menggunakan fetch() dengan then()#

Sekarang kita coba mengambil data artikel.

Misalnya, backend menyediakan:

GET http://localhost:3000/api/artikel

Kita bisa menulis:

fetch("http://localhost:3000/api/artikel")
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error("Gagal mengambil data:", error);
});

Mari kita pahami alurnya.

Pertama, fetch() mengirim HTTP request.

Kemudian .then() menerima objek Response ketika Promise dari fetch() berhasil diselesaikan.

Method:

response.json()

Membaca body response dan menguraikannya sebagai JSON secara asynchronous.

Karena response.json() juga mengembalikan Promise, kita menggunakan .then() berikutnya untuk mendapatkan hasilnya.

Jika terjadi kegagalan jaringan atau proses Promise ditolak, .catch() dapat menangani error tersebut.

Namun, ada satu hal penting.

Fetch tidak otomatis menolak Promise hanya karena server mengembalikan status HTTP 404 atau 500.

Kita perlu memeriksa status respons sendiri.

5. Memeriksa response.ok#

Objek Response mempunyai properti:

response.ok

Nilainya true ketika status HTTP berada dalam rentang 200 sampai 299.

Contohnya:

fetch("http://localhost:3000/api/artikel")
.then((response) => {
if (!response.ok) {
throw new Error(
`Request gagal dengan status ${response.status}`
);
}
return response.json();
})
.then((data) => {
console.log(data);
})
.catch((error) => {
console.error(error.message);
});

Dengan pemeriksaan tersebut, respons seperti 404 atau 500 akan diperlakukan sebagai error oleh alur kode kita.

Ini penting karena kegagalan HTTP dan kegagalan jaringan merupakan dua kondisi yang berbeda.

6. Mengenal async dan await#

Penulisan Promise menggunakan .then() valid dan sering digunakan.

Namun, ketika alurnya semakin panjang, sebagian pengembang lebih nyaman menggunakan async/await.

Kata kunci async digunakan untuk mendeklarasikan function asynchronous.

Kata kunci await digunakan untuk menunggu penyelesaian Promise di dalam konteks yang mendukungnya.

Contohnya:

async function ambilArtikel() {
const response = await fetch(
"http://localhost:3000/api/artikel"
);
if (!response.ok) {
throw new Error(
`Request gagal dengan status ${response.status}`
);
}
const data = await response.json();
console.log(data);
}
ambilArtikel().catch((error) => {
console.error(error.message);
});

Alurnya terlihat lebih menyerupai kode berurutan.

Namun, operasi tersebut tetap asynchronous.

await tidak berarti seluruh browser berhenti bekerja sampai request selesai.

7. Menangani Error dengan try…catch#

Ketika menggunakan async/await, kita sering memakai try...catch.

Contohnya:

async function ambilArtikel() {
try {
const response = await fetch(
"http://localhost:3000/api/artikel"
);
if (!response.ok) {
throw new Error(
`HTTP ${response.status}`
);
}
const data = await response.json();
console.log(data);
} catch (error) {
console.error(
"Gagal mengambil artikel:",
error.message
);
}
}
ambilArtikel();

Blok try berisi operasi yang mungkin menghasilkan error.

Blok catch menangani error tersebut.

Error dapat berasal dari:

  • Kegagalan jaringan.
  • Request yang dibatalkan.
  • Respons HTTP yang kita perlakukan sebagai error.
  • Body response yang tidak valid sebagai JSON.
  • Kesalahan lain ketika memproses data.

Dalam aplikasi nyata, pesan yang ditampilkan kepada pengguna sebaiknya mudah dipahami dan tidak membocorkan detail internal server.

8. Mengenal Same-Origin Policy dan CORS#

Sebelum membuat halaman frontend, ada konsep penting yang perlu dipahami: origin.

Origin terdiri dari skema, hostname, dan port.

Contohnya:

http://localhost:3000

Dan:

http://localhost:5500

Merupakan origin berbeda karena port-nya berbeda.

Begitu juga:

http://localhost:3000

Dan:

http://127.0.0.1:3000

Merupakan origin berbeda karena hostname-nya berbeda.

Browser menerapkan kebijakan keamanan yang disebut same-origin policy.

Untuk request lintas origin, mekanisme CORS (Cross-Origin Resource Sharing) memungkinkan server menyatakan origin mana yang diizinkan membaca respons melalui JavaScript browser.

Misalnya, frontend berjalan pada:

http://localhost:5500

Sedangkan backend berjalan pada:

http://localhost:3000

Jika server tidak mengizinkan origin frontend melalui CORS, browser dapat memblokir akses JavaScript terhadap respons.

CORS merupakan mekanisme yang diterapkan browser.

CORS bukan pengganti autentikasi, otorisasi, atau validasi data.

Karena itu, jangan menganggap API aman hanya karena CORS sudah dikonfigurasi.

9. Cara Menghindari Masalah CORS Saat Belajar#

Untuk latihan awal, kita akan menggunakan pendekatan yang lebih sederhana.

Frontend dan API dijalankan melalui satu HTTP server yang sama.

Dengan begitu, halaman HTML dan endpoint API menggunakan origin yang sama.

Struktur proyek latihan kita:

belajar-fetch-api/
├── server.js
└── public/
├── index.html
├── style.css
└── script.js

Node.js akan melayani halaman HTML, CSS, dan JavaScript sekaligus menyediakan endpoint API.

Kita tidak membutuhkan server frontend terpisah.

Keuntungannya:

  • Tidak perlu mengonfigurasi CORS untuk latihan ini.
  • Tidak perlu memasang framework.
  • Semua request menggunakan satu origin.
  • Lebih mudah memahami komunikasi frontend dan backend.

10. Menyiapkan Proyek#

Buat folder:

belajar-fetch-api/

Di dalamnya, buat file dan folder sesuai struktur sebelumnya.

Pastikan Node.js sudah tersedia:

Terminal window
node --version

Untuk latihan ini, kita akan menggunakan modul bawaan:

require("node:http");
require("node:fs/promises");
require("node:path");

Modul tersebut sudah tersedia pada Node.js.

Kita tidak perlu menjalankan npm install.

11. Membuat Backend API dan Server Frontend#

Buka file server.js.

Masukkan kode lengkap berikut:

const http = require("node:http");
const fs = require("node:fs/promises");
const path = require("node:path");
const HOST = "127.0.0.1";
const PORT = 3000;
const BATAS_BODY = 10 * 1024;
const folderPublic = path.join(__dirname, "public");
let artikel = [
{ id: 1, judul: "Belajar HTML", kategori: "HTML" },
{ id: 2, judul: "Belajar CSS", kategori: "CSS" },
{ id: 3, judul: "Belajar JavaScript", kategori: "JavaScript" }
];
let idBerikutnya = 4;
function kirimJSON(res, statusCode, data) {
res.writeHead(statusCode, {
"Content-Type": "application/json; charset=utf-8"
});
res.end(JSON.stringify(data));
}
function buatError(statusCode, pesan) {
const error = new Error(pesan);
error.statusCode = statusCode;
return error;
}
async function bacaJSON(req) {
const tipeKonten = req.headers["content-type"] || "";
if (!/^application\/json(?:\s*;|$)/i.test(tipeKonten)) {
throw buatError(
415,
"Gunakan Content-Type: application/json."
);
}
const chunks = [];
let totalUkuran = 0;
let terlaluBesar = false;
for await (const chunk of req) {
totalUkuran += chunk.length;
if (totalUkuran > BATAS_BODY) {
terlaluBesar = true;
}
if (!terlaluBesar) {
chunks.push(chunk);
}
}
if (terlaluBesar) {
throw buatError(413, "Ukuran request terlalu besar.");
}
const body = Buffer.concat(chunks).toString("utf8");
try {
return JSON.parse(body);
} catch {
throw buatError(400, "Format JSON tidak valid.");
}
}
function validasiArtikel(data) {
return (
data !== null &&
typeof data === "object" &&
!Array.isArray(data) &&
typeof data.judul === "string" &&
data.judul.trim().length > 0 &&
data.judul.trim().length <= 150 &&
typeof data.kategori === "string" &&
data.kategori.trim().length > 0 &&
data.kategori.trim().length <= 50
);
}
async function kirimFile(res, namaFile, contentType) {
const lokasi = path.join(folderPublic, namaFile);
const isi = await fs.readFile(lokasi);
res.writeHead(200, {
"Content-Type": contentType
});
res.end(isi);
}
async function tanganiRequest(req, res) {
const url = new URL(req.url, "http://localhost");
const pathname = url.pathname;
const method = req.method;
if (method === "GET" && pathname === "/") {
await kirimFile(
res,
"index.html",
"text/html; charset=utf-8"
);
return;
}
if (method === "GET" && pathname === "/style.css") {
await kirimFile(
res,
"style.css",
"text/css; charset=utf-8"
);
return;
}
if (method === "GET" && pathname === "/script.js") {
await kirimFile(
res,
"script.js",
"text/javascript; charset=utf-8"
);
return;
}
if (method === "GET" && pathname === "/api/artikel") {
kirimJSON(res, 200, artikel);
return;
}
if (method === "POST" && pathname === "/api/artikel") {
const data = await bacaJSON(req);
if (!validasiArtikel(data)) {
kirimJSON(res, 400, {
pesan:
"Judul wajib diisi (maksimal 150 karakter) dan kategori wajib diisi (maksimal 50 karakter)."
});
return;
}
const artikelBaru = {
id: idBerikutnya++,
judul: data.judul.trim(),
kategori: data.kategori.trim()
};
artikel.push(artikelBaru);
res.setHeader(
"Location",
`/api/artikel/${artikelBaru.id}`
);
kirimJSON(res, 201, artikelBaru);
return;
}
if (pathname === "/api/artikel") {
res.setHeader("Allow", "GET, POST");
kirimJSON(res, 405, {
pesan: "Method tidak diizinkan."
});
return;
}
kirimJSON(res, 404, {
pesan: "Halaman atau endpoint tidak ditemukan."
});
}
const server = http.createServer((req, res) => {
tanganiRequest(req, res).catch((error) => {
console.error("Server error:", error.message);
if (res.headersSent) {
res.destroy();
return;
}
const statusCode =
Number.isInteger(error.statusCode) &&
error.statusCode >= 400 &&
error.statusCode < 500
? error.statusCode
: 500;
kirimJSON(res, statusCode, {
pesan:
statusCode === 500
? "Terjadi kesalahan pada server."
: error.message
});
});
});
server.on("error", (error) => {
console.error("Gagal menjalankan server:", error.message);
});
server.listen(PORT, HOST, () => {
console.log(`Buka http://localhost:${PORT}`);
});

Server tersebut memiliki dua tanggung jawab.

Pertama, melayani file frontend:

GET /
GET /style.css
GET /script.js

Kedua, menyediakan endpoint API:

GET /api/artikel
POST /api/artikel

Untuk latihan ini, kita sengaja hanya menggunakan operasi membaca dan menambahkan artikel.

Operasi PUT dan DELETE sudah kita pelajari pada artikel sebelumnya.

Catatan keamanan#

File yang dilayani server ditentukan melalui daftar route yang eksplisit.

Kita tidak menerima nama file sembarang dari URL untuk langsung dibaca dari sistem.

Pendekatan ini menghindari kebutuhan menangani path traversal pada contoh sederhana.

Namun, server ini tetap hanya ditujukan untuk pembelajaran lokal.

Belum ada autentikasi, penyimpanan permanen, atau perlindungan produksi yang lengkap.

12. Membuat Struktur HTML#

Sekarang buka public/index.html.

Masukkan kode berikut:

<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
>
<title>Daftar Artikel - BapakGabut</title>
<link rel="stylesheet" href="/style.css">
<script src="/script.js" defer></script>
</head>
<body>
<main class="container">
<header>
<h1>Daftar Artikel BapakGabut</h1>
<p>
Belajar menghubungkan frontend dan backend
menggunakan Fetch API.
</p>
</header>
<section aria-labelledby="judul-daftar">
<div class="judul-bagian">
<h2 id="judul-daftar">Artikel Tersedia</h2>
<button id="tombol-refresh" type="button">
Muat Ulang
</button>
</div>
<p id="status-daftar" role="status" aria-live="polite"></p>
<ul id="daftar-artikel" class="daftar-artikel"></ul>
</section>
<section aria-labelledby="judul-form">
<h2 id="judul-form">Tambah Artikel</h2>
<form id="form-artikel">
<label for="judul">Judul Artikel</label>
<input
id="judul"
name="judul"
type="text"
maxlength="150"
required
>
<label for="kategori">Kategori</label>
<input
id="kategori"
name="kategori"
type="text"
maxlength="50"
required
>
<button id="tombol-simpan" type="submit">
Simpan Artikel
</button>
</form>
<p id="status-form" role="status" aria-live="polite"></p>
</section>
</main>
</body>
</html>

Struktur HTML tersebut mempunyai beberapa elemen penting.

Daftar artikel#

Elemen:

<ul id="daftar-artikel"></ul>

Akan diisi menggunakan JavaScript setelah data diterima dari API.

Status daftar#

Elemen:

<p id="status-daftar" role="status" aria-live="polite"></p>

Digunakan untuk menampilkan pesan loading, error, atau informasi lainnya.

Formulir#

Formulir mempunyai dua input:

  • Judul artikel.
  • Kategori.

Ketika formulir dikirim, JavaScript akan mencegah submit HTML biasa dan mengirim request melalui Fetch API.

13. Menambahkan CSS Sederhana#

Sekarang buka public/style.css.

Masukkan kode berikut:

* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, sans-serif;
line-height: 1.6;
background: #f3f4f6;
color: #111827;
}
.container {
width: min(100% - 32px, 760px);
margin: 40px auto;
}
header,
section {
background: white;
padding: 24px;
border-radius: 12px;
margin-bottom: 20px;
}
h1,
h2 {
margin-top: 0;
}
.judul-bagian {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.daftar-artikel {
list-style: none;
padding: 0;
}
.daftar-artikel li {
padding: 12px;
border: 1px solid #d1d5db;
border-radius: 8px;
margin-bottom: 10px;
}
.daftar-artikel strong {
display: block;
}
label {
display: block;
margin: 14px 0 6px;
font-weight: bold;
}
input {
display: block;
width: 100%;
padding: 10px;
border: 1px solid #9ca3af;
border-radius: 6px;
font: inherit;
}
button {
padding: 10px 16px;
border: none;
border-radius: 6px;
background: #2563eb;
color: white;
cursor: pointer;
font: inherit;
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
button:focus-visible,
input:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 2px;
}
#status-daftar,
#status-form {
min-height: 24px;
}
@media (max-width: 480px) {
.container {
margin: 16px auto;
}
header,
section {
padding: 16px;
}
}

CSS tersebut hanya digunakan untuk membuat halaman lebih nyaman dibaca.

Fokus utama artikel ini tetap pada komunikasi frontend dan backend.

14. Mengambil Data Menggunakan Fetch API#

Sekarang kita mulai menulis JavaScript browser.

Buka public/script.js.

Pertama, ambil elemen HTML yang diperlukan.

const daftarArtikel = document.querySelector(
"#daftar-artikel"
);
const statusDaftar = document.querySelector(
"#status-daftar"
);
const tombolRefresh = document.querySelector(
"#tombol-refresh"
);

Kemudian buat function untuk mengambil data.

async function ambilArtikel() {
statusDaftar.textContent = "Memuat artikel...";
try {
const response = await fetch("/api/artikel");
if (!response.ok) {
throw new Error(
`Gagal mengambil artikel. HTTP ${response.status}`
);
}
const data = await response.json();
console.log(data);
statusDaftar.textContent = "Artikel berhasil dimuat.";
} catch (error) {
statusDaftar.textContent =
"Gagal memuat artikel. Silakan coba lagi.";
console.error(error);
}
}

Perhatikan URL:

fetch("/api/artikel");

Kita tidak perlu menuliskan hostname dan port.

Karena frontend dan backend berjalan pada origin yang sama, browser menggunakan origin halaman saat ini.

Jika halaman dibuka melalui:

http://localhost:3000

Maka request menuju:

http://localhost:3000/api/artikel

15. Menampilkan Data ke HTML#

Mengambil data saja belum cukup.

Kita ingin menampilkannya di halaman.

Buat function:

function tampilkanArtikel(data) {
daftarArtikel.replaceChildren();
for (const item of data) {
const li = document.createElement("li");
const judul = document.createElement("strong");
judul.textContent = item.judul;
const kategori = document.createElement("span");
kategori.textContent = `Kategori: ${item.kategori}`;
li.append(judul, kategori);
daftarArtikel.append(li);
}
}

Function tersebut:

  1. Mengosongkan daftar artikel sebelumnya.
  2. Membuat elemen <li> untuk setiap artikel.
  3. Membuat elemen judul dan kategori.
  4. Menambahkan elemen ke halaman.

Mengapa menggunakan textContent?#

Karena data artikel berasal dari API.

Walaupun backend kita sederhana, data dari sumber eksternal tetap harus diperlakukan dengan hati-hati.

Penggunaan:

judul.textContent = item.judul;

Menampilkan nilai sebagai teks.

Ini lebih aman untuk teks biasa daripada memasukkan data yang belum dipercaya langsung ke innerHTML.

16. Menambahkan Loading State#

Ketika request sedang berlangsung, pengguna perlu mengetahui bahwa aplikasi sedang bekerja.

Kita dapat menonaktifkan tombol refresh sementara.

Contohnya:

tombolRefresh.disabled = true;

Kemudian mengaktifkannya kembali setelah request selesai.

Untuk memastikan proses pemulihan berjalan baik, gunakan finally.

Contohnya:

async function ambilArtikel() {
tombolRefresh.disabled = true;
statusDaftar.textContent = "Memuat artikel...";
try {
const response = await fetch("/api/artikel");
if (!response.ok) {
throw new Error(
`HTTP ${response.status}`
);
}
const data = await response.json();
tampilkanArtikel(data);
statusDaftar.textContent =
`${data.length} artikel berhasil dimuat.`;
} catch (error) {
statusDaftar.textContent =
"Gagal memuat artikel. Silakan coba lagi.";
console.error(error);
} finally {
tombolRefresh.disabled = false;
}
}

Blok finally dijalankan setelah try atau catch selesai, sehingga cocok untuk memulihkan kondisi tombol.

17. Mengirim Data Menggunakan POST#

Sekarang kita ingin menambahkan artikel baru melalui formulir.

Untuk mengirim data JSON menggunakan Fetch API, kita dapat menulis:

const response = await fetch("/api/artikel", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
judul: "Belajar Fetch API",
kategori: "JavaScript"
})
});

Mari kita pahami.

method#

Bagian:

method: "POST"

Menentukan HTTP method yang digunakan.

headers#

Bagian:

headers: {
"Content-Type": "application/json"
}

Memberi tahu server bahwa body dikirim dalam format JSON.

body#

Bagian:

body: JSON.stringify(data)

Mengubah object JavaScript menjadi string JSON sebelum dikirim.

Server kemudian membaca body tersebut menggunakan function bacaJSON() yang sudah kita buat.

18. Membaca Pesan Error dari Backend#

Backend kita mengembalikan pesan JSON ketika request gagal.

Contohnya:

{
"pesan": "Judul dan kategori wajib diisi."
}

Kita bisa membaca pesan tersebut agar frontend dapat menampilkan informasi yang lebih berguna.

Contoh:

const response = await fetch("/api/artikel", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data)
});
const hasil = await response.json();
if (!response.ok) {
throw new Error(
hasil.pesan || `HTTP ${response.status}`
);
}

Pada contoh ini, kita membaca body JSON terlebih dahulu.

Kemudian kita memeriksa apakah status HTTP menunjukkan keberhasilan.

Jika tidak, kita menggunakan pesan dari backend.

Untuk aplikasi produksi, pesan dari backend sebaiknya dirancang agar aman ditampilkan kepada pengguna.

Jangan menampilkan stack trace atau detail internal server secara langsung.

19. Membuat JavaScript Frontend Lengkap#

Sekarang kita gabungkan seluruh kode frontend.

Ganti isi public/script.js dengan:

const daftarArtikel = document.querySelector(
"#daftar-artikel"
);
const statusDaftar = document.querySelector(
"#status-daftar"
);
const tombolRefresh = document.querySelector(
"#tombol-refresh"
);
const formArtikel = document.querySelector(
"#form-artikel"
);
const tombolSimpan = document.querySelector(
"#tombol-simpan"
);
const statusForm = document.querySelector(
"#status-form"
);
function tampilkanArtikel(data) {
daftarArtikel.replaceChildren();
for (const item of data) {
const li = document.createElement("li");
const judul = document.createElement("strong");
judul.textContent = item.judul;
const kategori = document.createElement("span");
kategori.textContent = `Kategori: ${item.kategori}`;
li.append(judul, kategori);
daftarArtikel.append(li);
}
}
async function ambilArtikel() {
tombolRefresh.disabled = true;
statusDaftar.textContent = "Memuat artikel...";
try {
const response = await fetch("/api/artikel");
if (!response.ok) {
throw new Error(
`Gagal mengambil artikel. HTTP ${response.status}`
);
}
const data = await response.json();
if (!Array.isArray(data)) {
throw new Error("Format data artikel tidak sesuai.");
}
tampilkanArtikel(data);
statusDaftar.textContent =
`${data.length} artikel berhasil dimuat.`;
} catch (error) {
statusDaftar.textContent =
"Gagal memuat artikel. Silakan coba lagi.";
console.error(error);
} finally {
tombolRefresh.disabled = false;
}
}
async function simpanArtikel(event) {
event.preventDefault();
const judul = document.querySelector("#judul").value.trim();
const kategori = document.querySelector(
"#kategori"
).value.trim();
if (!judul || !kategori) {
statusForm.textContent =
"Judul dan kategori wajib diisi.";
return;
}
tombolSimpan.disabled = true;
statusForm.textContent = "Menyimpan artikel...";
try {
const response = await fetch("/api/artikel", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
judul,
kategori
})
});
const hasil = await response.json();
if (!response.ok) {
throw new Error(
hasil.pesan || `HTTP ${response.status}`
);
}
statusForm.textContent =
`Artikel "${hasil.judul}" berhasil ditambahkan.`;
formArtikel.reset();
await ambilArtikel();
} catch (error) {
statusForm.textContent =
`Gagal menyimpan artikel: ${error.message}`;
console.error(error);
} finally {
tombolSimpan.disabled = false;
}
}
tombolRefresh.addEventListener("click", ambilArtikel);
formArtikel.addEventListener("submit", simpanArtikel);
ambilArtikel();

Sekarang halaman kita sudah bisa berkomunikasi dengan backend.

Ketika halaman dibuka, JavaScript otomatis mengambil daftar artikel.

Ketika tombol Muat Ulang ditekan, frontend mengirim request GET lagi.

Ketika formulir dikirim, frontend mengirim request POST.

Jika penyimpanan berhasil, daftar artikel diperbarui tanpa memuat ulang seluruh halaman.

20. Menjalankan Aplikasi#

Pastikan struktur folder sesuai:

belajar-fetch-api/
├── server.js
└── public/
├── index.html
├── style.css
└── script.js

Buka terminal pada folder belajar-fetch-api.

Jalankan:

Terminal window
node server.js

Jika berhasil, terminal menampilkan:

Buka http://localhost:3000

Kemudian buka browser:

http://localhost:3000

Halaman akan menampilkan daftar artikel awal.

Coba isi formulir:

Judul Artikel: Belajar Fetch API
Kategori: JavaScript

Kemudian tekan tombol Simpan Artikel.

Jika berhasil:

  1. Frontend mengirim request POST.
  2. Backend memvalidasi data.
  3. Backend menambahkan artikel ke array.
  4. Backend mengembalikan status 201.
  5. Frontend menampilkan pesan keberhasilan.
  6. Frontend mengambil ulang daftar artikel.
  7. Artikel baru muncul di halaman.

Kita baru saja menghubungkan frontend dan backend menggunakan JavaScript murni.

21. Melihat Komunikasi API melalui Network#

Sekarang buka Developer Tools browser.

Pilih tab Network.

Kemudian muat ulang halaman.

Cari request:

GET /api/artikel

Perhatikan informasi berikut:

  • Request method.
  • Request URL.
  • Status code.
  • Response headers.
  • Response body.

Sekarang tambahkan artikel melalui formulir.

Cari request:

POST /api/artikel

Perhatikan request payload yang dikirim.

Contohnya:

{
"judul": "Belajar Fetch API",
"kategori": "JavaScript"
}

Lihat juga respons yang dikembalikan backend.

Jika berhasil, statusnya:

201 Created

Ini membantu kita melihat secara langsung bagaimana browser dan backend berkomunikasi.

22. Mengapa Data Hilang Setelah Server Restart?#

Pada latihan ini, backend masih menyimpan artikel dalam array JavaScript.

Contohnya:

let artikel = [
{ id: 1, judul: "Belajar HTML", kategori: "HTML" }
];

Ketika server dihentikan, data yang hanya berada di memori tidak disimpan secara permanen.

Ketika server dijalankan kembali, array dibuat ulang dari nilai awal.

Karena itu, artikel yang ditambahkan melalui formulir akan hilang setelah proses server dimulai ulang.

Ini bukan kesalahan Fetch API.

Fetch API hanya menangani komunikasi HTTP.

Penyimpanan data merupakan tanggung jawab backend dan sistem penyimpanan yang digunakan.

Kita akan membahas penyimpanan permanen pada seri Database.

23. Kesalahan yang Sering Terjadi#

Membuka HTML langsung menggunakan file://#

Jika kita membuka:

file:///C:/belajar-fetch-api/public/index.html

Halaman tidak menggunakan origin server HTTP yang kita siapkan.

Akibatnya, request relatif seperti:

fetch("/api/artikel")

Tidak akan menuju backend sesuai rancangan latihan.

Buka halaman melalui:

http://localhost:3000

Server belum berjalan#

Jika Node.js belum dijalankan, browser tidak dapat menghubungi backend.

Pastikan terminal masih menjalankan:

Terminal window
node server.js

Port sudah digunakan#

Jika muncul error EADDRINUSE, kemungkinan port 3000 sudah digunakan proses lain.

Hentikan proses sebelumnya atau ubah port server.

Menggunakan hostname atau port yang berbeda#

Perhatikan perbedaan antara:

localhost

Dan:

127.0.0.1

Serta perbedaan port.

Untuk latihan ini, selalu buka frontend melalui alamat yang ditampilkan server.

Lupa menggunakan await#

Kode:

const response = fetch("/api/artikel");

Menghasilkan Promise, bukan objek Response yang sudah tersedia.

Gunakan:

const response = await fetch("/api/artikel");

Di dalam async function.

Menganggap fetch gagal otomatis ketika status 404#

Tidak.

Periksa:

response.ok

Atau:

response.status

Untuk mengetahui hasil HTTP request.

Menggunakan innerHTML untuk menampilkan data sembarang#

Hindari memasukkan data yang tidak dipercaya langsung ke innerHTML.

Untuk teks biasa, gunakan textContent.

Menganggap validasi frontend sudah cukup#

Backend tetap harus memvalidasi data.

Pengguna dapat mengirim request langsung tanpa melalui formulir HTML.

Menampilkan seluruh detail error internal#

Frontend sebaiknya menampilkan pesan yang aman dan mudah dipahami.

Detail teknis error internal lebih tepat dicatat pada log backend.

24. Latihan Mandiri#

Sekarang kita coba mengembangkan aplikasi.

Latihan 1: Menampilkan jumlah artikel#

Tambahkan elemen HTML yang menunjukkan jumlah artikel.

Perbarui nilainya setiap kali data berhasil diambil.

Latihan 2: Menambahkan tombol kosongkan formulir#

Tambahkan tombol untuk menghapus seluruh isi input formulir.

Pastikan tombol tersebut tidak mengirim request POST.

Latihan 3: Menampilkan kategori dengan gaya berbeda#

Ubah CSS agar kategori artikel terlihat seperti label.

Latihan 4: Menampilkan pesan ketika daftar kosong#

Jika API mengembalikan array kosong, tampilkan pesan:

Belum ada artikel.

Jangan hanya menampilkan daftar kosong tanpa penjelasan.

Latihan 5: Menguji error jaringan#

Jalankan halaman terlebih dahulu.

Kemudian hentikan server Node.js.

Tekan tombol Muat Ulang.

Amati bagaimana frontend menampilkan pesan error.

Setelah itu, jalankan kembali server dan coba lagi.

Latihan 6: Menambahkan tombol hapus#

Sebagai tantangan tambahan, kembangkan backend dengan endpoint DELETE seperti pada artikel sebelumnya.

Kemudian tambahkan tombol hapus pada setiap artikel.

Gunakan Fetch API dengan method DELETE.

Pastikan pengguna mendapat konfirmasi sebelum penghapusan dilakukan.

25. Apa yang Sudah Kita Pelajari?#

Pada artikel ini, kita berhasil menghubungkan frontend dan backend menggunakan Fetch API.

Kita sudah mempraktikkan:

  • Mengirim HTTP request melalui browser.
  • Mengenal Promise.
  • Menggunakan .then() dan .catch().
  • Menggunakan async/await.
  • Menangani error dengan try...catch.
  • Memeriksa response.ok.
  • Membaca JSON menggunakan response.json().
  • Mengirim JSON menggunakan JSON.stringify().
  • Membuat loading state.
  • Menampilkan data API ke HTML.
  • Mengirim data formulir menggunakan POST.
  • Memahami konsep origin dan CORS.
  • Menggunakan satu HTTP server untuk frontend dan API.

Dengan pengetahuan tersebut, kita sudah dapat membuat halaman web yang berkomunikasi dengan backend sendiri.

26. Penutup#

Selamat, Gabuters!

Kita berhasil membuat aplikasi sederhana yang menghubungkan HTML, CSS, JavaScript, dan backend Node.js native.

Frontend dapat mengambil data artikel dan mengirim data baru melalui Fetch API.

Backend menerima request, memvalidasi data, dan mengembalikan respons JSON.

Kita juga belajar bahwa komunikasi API membutuhkan penanganan error dan pengalaman pengguna yang baik.

Namun, kita belum selesai.

Pada seri JavaScript sebelumnya, kita sudah membuat halaman profil interaktif.

Bagaimana jika halaman profil tersebut kita kembangkan agar formulir kontaknya benar-benar mengirim data ke backend?

Bagaimana jika backend memvalidasi pesan dan menyediakan endpoint untuk melihat pesan yang diterima?

Pada artikel berikutnya, kita akan membuat Mini Project: Membuat API untuk Halaman Profil Interaktif.

Kita akan menggabungkan seluruh pembelajaran dari seri Backend dan API dalam satu proyek kecil.

Tetap tanpa Express dan tanpa database terlebih dahulu.

Sampai jumpa di artikel berikutnya, Gabuters!

Bagian 5 dari 6Tulisan ini bagian dari seri

Bagikan Artikel

Buat poster berbagi atau salin tautan artikel ini.

Lanjut membaca

Bacaan terkait

Berdasarkan tag dan kategori yang sama

Coba jalur lain

Pilihan konsisten dari artikel lain