Mini Project: Membuat API untuk Halaman Profil Interaktif
Halo, Gabuters!
Akhirnya kita sampai pada artikel terakhir seri Backend dan API dengan JavaScript.
Pada artikel sebelumnya, kita berhasil menghubungkan frontend dan backend menggunakan Fetch API.
Kita sudah mempelajari HTTP, JSON, routing, REST API, operasi CRUD, Promise, dan async/await.
Sekarang waktunya menggabungkan pengetahuan tersebut menjadi sebuah mini project.
Masih ingat mini project halaman profil interaktif dari seri JavaScript?
Pada proyek tersebut, kita membuat halaman profil yang memiliki beberapa fitur interaktif, termasuk formulir kontak.
Namun, formulirnya belum benar-benar mengirim pesan ke server.
Ketika tombol kirim ditekan, JavaScript hanya memvalidasi input dan menampilkan notifikasi di browser.
Pada artikel ini, kita akan mengembangkan konsep tersebut.
Formulir kontak akan mengirim data menggunakan Fetch API. Backend akan menerima dan memvalidasi pesan, kemudian menyimpannya sementara di memori server.
Kita juga akan membuat endpoint untuk melihat pesan yang diterima.
Semua menggunakan JavaScript murni dan Node.js native, tanpa Express.
Mari kita mulai!
1. Apa yang Akan Kita Buat?
Mini project kita berupa halaman profil sederhana dengan beberapa fitur:
- Menampilkan informasi profil.
- Mengganti tema terang dan gelap.
- Menampilkan salam interaktif.
- Menghitung jumlah karakter pesan.
- Mengirim formulir kontak ke backend.
- Memvalidasi data di frontend dan backend.
- Menampilkan status pengiriman.
- Menyediakan endpoint untuk melihat jumlah pesan yang diterima.
Alur pengiriman pesannya seperti ini:
Pengunjung mengisi formulir | vJavaScript memvalidasi input | vfetch() mengirim POST /api/pesan | vBackend membaca body JSON | vBackend memvalidasi data | vPesan disimpan di memori | vBackend mengirim response JSON | vFrontend menampilkan hasilKita akan membuat dua endpoint utama:
| Method | Endpoint | Fungsi |
|---|---|---|
| GET | /api/profil | Mengambil informasi profil |
| POST | /api/pesan | Menerima pesan kontak |
Selain itu, kita akan menambahkan:
GET /api/statusEndpoint ini menampilkan informasi sederhana mengenai server dan jumlah pesan yang sudah diterima sejak proses server berjalan.
Penting: Penyimpanan pesan pada proyek ini hanya menggunakan array di memori. Pesan tidak disimpan permanen, tidak dikirim melalui email, dan akan hilang ketika server dimulai ulang.
2. Mengapa Kita Tidak Langsung Menggunakan Database?
Tujuan mini project ini adalah memahami komunikasi frontend dan backend.
Jika kita langsung menambahkan database, konfigurasi koneksi, dan sistem autentikasi, pembahasannya akan menjadi terlalu luas untuk satu artikel.
Karena itu, kita akan menggunakan array:
const pesanMasuk = [];Setiap pesan yang valid akan dimasukkan ke array tersebut.
Pendekatan ini cukup untuk mendemonstrasikan proses penerimaan data.
Namun, ada beberapa keterbatasan:
- Data hilang ketika server dimulai ulang.
- Data tidak dibagikan secara otomatis antarbeberapa proses server.
- Tidak cocok untuk menyimpan pesan penting.
- Tidak menyediakan penyimpanan permanen.
Kita akan mempelajari penyimpanan data menggunakan database pada seri berikutnya.
3. Menyiapkan Struktur Proyek
Buat folder:
mini-project-api-profil/Kemudian siapkan struktur berikut:
mini-project-api-profil/├── server.js└── public/ ├── index.html ├── style.css └── script.jsPenjelasannya:
server.js: backend HTTP server.index.html: struktur halaman profil.style.css: tampilan dan tema halaman.script.js: interaksi browser dan komunikasi API.
Kita menggunakan satu server untuk melayani frontend sekaligus endpoint API.
Dengan begitu, frontend dan backend berada pada origin yang sama dan kita tidak perlu mengatur CORS untuk latihan ini.
Pastikan Node.js sudah terpasang.
Periksa melalui terminal:
node --versionGunakan versi LTS modern.
Kita tidak membutuhkan npm install karena seluruh modul backend sudah tersedia di Node.js.
4. Merancang API Sebelum Menulis Kode
Sebelum membuat server, kita tentukan kontrak API sederhana.
GET /api/profil
Endpoint ini mengembalikan informasi profil.
Contoh response:
{ "nama": "BapakGabut", "tagline": "Ngoding, main game, dan ngulik apa saja.", "minat": [ "Programming", "Gaming", "Teknologi" ]}POST /api/pesan
Endpoint ini menerima body JSON:
{ "nama": "Gabuters", "email": "gabuters@example.com", "pesan": "Halo, saya ingin bertanya."}Jika valid, backend mengembalikan:
{ "berhasil": true, "pesan": "Pesan berhasil diterima.", "id": 1}Dengan status:
201 CreatedJika data tidak valid, backend mengembalikan status 400 dengan pesan kesalahan.
GET /api/status
Endpoint ini mengembalikan:
{ "status": "ok", "jumlahPesan": 1}Jumlah pesan akan bertambah setiap kali backend menerima pesan yang valid.
Untuk latihan, endpoint ini hanya menampilkan jumlah pesan, bukan isi pesan atau alamat email pengunjung.
Kita sengaja tidak menyediakan endpoint publik untuk membaca seluruh pesan karena data formulir kontak dapat mengandung informasi pribadi.
5. Menyiapkan Backend Node.js
Buka file server.js.
Kita akan menggunakan tiga modul bawaan Node.js:
const http = require("node:http");const fs = require("node:fs/promises");const path = require("node:path");Fungsi masing-masing:
node:http: membuat HTTP server.node:fs/promises: membaca file secara asynchronous.node:path: menyusun path file.
Kemudian siapkan konfigurasi dasar:
const HOST = "127.0.0.1";const PORT = 3000;
const folderPublic = path.join(__dirname, "public");Kita menggunakan alamat loopback 127.0.0.1 agar server latihan hanya menerima koneksi lokal.
Proyek ini memang ditujukan untuk dijalankan di komputer sendiri, bukan langsung dipublikasikan ke internet.
6. Menyiapkan Penyimpanan Pesan Sementara
Buat array untuk menyimpan pesan:
const pesanMasuk = [];
let idBerikutnya = 1;Ketika sebuah pesan valid diterima, kita akan menambahkan object ke array.
Contohnya:
{ id: 1, nama: "Gabuters", email: "gabuters@example.com", pesan: "Halo, saya ingin bertanya.", diterimaPada: "2026-10-09T12:30:00.000Z"}Field diterimaPada akan diisi menggunakan waktu server ketika pesan diterima.
Nilai waktu di atas hanya contoh.
Kita tidak akan menampilkan seluruh isi array melalui endpoint publik.
Untuk melihat apakah jumlah pesan bertambah, kita cukup menggunakan endpoint /api/status.
7. Membuat Function Pengirim JSON
Agar kode lebih rapi, buat function:
function kirimJSON(res, statusCode, data) { res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
res.end(JSON.stringify(data));}Header:
Content-Type: application/jsonMenunjukkan bahwa response berisi JSON.
Sedangkan:
Cache-Control: no-storeMeminta cache tidak menyimpan response tersebut.
Ini berguna untuk endpoint yang mengembalikan informasi dinamis.
Namun, header cache bukan pengganti pengamanan data atau autentikasi.
8. Membaca Body JSON dari Request
Ketika frontend mengirim request POST, data berada pada request body.
Karena kita tidak menggunakan Express, body akan dibaca melalui stream bawaan Node.js.
Kita juga akan membatasi ukuran body maksimal 10 KB.
Contohnya:
async function bacaJSON(req) { const chunks = [];
for await (const chunk of req) { chunks.push(chunk); }
const teks = Buffer.concat(chunks).toString("utf8");
return JSON.parse(teks);}Kode tersebut menggambarkan proses dasar pembacaan body.
Namun, contoh sederhana ini belum membatasi ukuran data dan belum membedakan jenis kesalahan.
Pada kode lengkap nanti, kita akan menambahkan:
- Pemeriksaan
Content-Type. - Batas ukuran body.
- Penanganan JSON tidak valid.
- Status code yang sesuai.
9. Menentukan Aturan Validasi Pesan
Formulir kontak akan mempunyai tiga field:
- Nama.
- Email.
- Pesan.
Aturan yang kita gunakan:
| Field | Aturan |
|---|---|
| Nama | Wajib, maksimal 80 karakter |
| Wajib, format sederhana, maksimal 254 karakter | |
| Pesan | Wajib, maksimal 200 karakter |
Validasi email yang digunakan hanya pemeriksaan dasar untuk pembelajaran.
Validasi ini tidak menjamin bahwa alamat email benar-benar ada atau dapat menerima pesan.
Untuk aplikasi nyata, kebutuhan validasi dapat lebih kompleks dan mungkin memerlukan proses verifikasi.
Kita akan melakukan validasi dua kali:
- Di frontend, untuk membantu pengguna.
- Di backend, untuk memastikan request yang diterima memenuhi aturan aplikasi.
Validasi backend tetap diperlukan karena request dapat dikirim tanpa melalui formulir yang kita buat.
10. Membuat Backend Lengkap
Sekarang ganti seluruh isi server.js dengan kode 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");
const pesanMasuk = [];let idBerikutnya = 1;
const profil = { nama: "BapakGabut", tagline: "Ngoding, main game, dan ngulik apa saja.", minat: ["Programming", "Gaming", "Teknologi"]};
function buatError(statusCode, pesan) { const error = new Error(pesan); error.statusCode = statusCode; return error;}
function kirimJSON(res, statusCode, data) { res.writeHead(statusCode, { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" });
res.end(JSON.stringify(data));}
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 pesan terlalu besar."); }
const teks = Buffer.concat(chunks).toString("utf8");
try { return JSON.parse(teks); } catch { throw buatError(400, "Format JSON tidak valid."); }}
function validasiPesan(data) { if ( data === null || typeof data !== "object" || Array.isArray(data) ) { return "Data pesan harus berupa object JSON."; }
if ( typeof data.nama !== "string" || data.nama.trim().length === 0 || data.nama.trim().length > 80 ) { return "Nama wajib diisi, maksimal 80 karakter."; }
if ( typeof data.email !== "string" || data.email.trim().length === 0 || data.email.trim().length > 254 ) { return "Email wajib diisi, maksimal 254 karakter."; }
const email = data.email.trim();
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { return "Format email tidak valid."; }
if ( typeof data.pesan !== "string" || data.pesan.trim().length === 0 || data.pesan.trim().length > 200 ) { return "Pesan wajib diisi, maksimal 200 karakter."; }
return null;}
async function kirimFile(res, namaFile, tipeKonten) { const lokasi = path.join(folderPublic, namaFile); const isi = await fs.readFile(lokasi);
res.writeHead(200, { "Content-Type": tipeKonten });
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/profil") { kirimJSON(res, 200, profil); return; }
if (method === "GET" && pathname === "/api/status") { kirimJSON(res, 200, { status: "ok", jumlahPesan: pesanMasuk.length }); return; }
if (method === "POST" && pathname === "/api/pesan") { const data = await bacaJSON(req); const kesalahan = validasiPesan(data);
if (kesalahan) { kirimJSON(res, 400, { berhasil: false, pesan: kesalahan }); return; }
const pesanBaru = { id: idBerikutnya++, nama: data.nama.trim(), email: data.email.trim(), pesan: data.pesan.trim(), diterimaPada: new Date().toISOString() };
pesanMasuk.push(pesanBaru);
kirimJSON(res, 201, { berhasil: true, pesan: "Pesan berhasil diterima.", id: pesanBaru.id }); return; }
if ( pathname === "/api/profil" || pathname === "/api/status" || pathname === "/api/pesan" ) { res.setHeader( "Allow", pathname === "/api/pesan" ? "POST" : "GET" );
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, { berhasil: false, 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(`Server berjalan di http://localhost:${PORT}`);});Backend kita sekarang memiliki tiga endpoint API:
GET /api/profilGET /api/statusPOST /api/pesanKita juga melayani tiga file frontend:
GET /GET /style.cssGET /script.jsMengapa data pesan tidak dikembalikan seluruhnya?
Karena formulir kontak dapat berisi informasi pribadi seperti nama, email, dan isi pesan.
Jika kita membuat endpoint publik yang mengembalikan seluruh pesan tanpa autentikasi, siapa pun yang mengetahui endpoint tersebut berpotensi membaca data pengunjung.
Karena itu, pada latihan ini kita hanya menampilkan jumlah pesan melalui /api/status.
Untuk aplikasi nyata, akses ke data pesan harus dilindungi menggunakan autentikasi dan otorisasi yang sesuai.
Apakah kode ini siap digunakan untuk website publik?
Belum.
Kode ini dirancang untuk latihan lokal.
Belum ada autentikasi, rate limiting, penyimpanan permanen, proteksi spam, atau pengaturan operasional yang lengkap.
Jangan menggunakan formulir ini untuk mengumpulkan pesan pengunjung sungguhan sebelum sistem disiapkan dengan benar.
11. Membuat Halaman 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>Profil Interaktif - BapakGabut</title> <link rel="stylesheet" href="/style.css"> <script src="/script.js" defer></script></head><body> <main class="container"> <header class="kartu"> <div class="baris-header"> <h1>Profil BapakGabut</h1>
<button id="tombol-tema" type="button" aria-pressed="false" > Mode Gelap </button> </div>
<p id="tagline">Memuat profil...</p>
<p id="minat"></p>
<button id="tombol-salam" type="button"> Sapa Saya </button>
<p id="hasil-salam" role="status"></p> </header>
<section class="kartu" aria-labelledby="judul-kontak"> <h2 id="judul-kontak">Kirim Pesan</h2>
<p> Formulir ini adalah demonstrasi lokal. Pesan hanya disimpan sementara di memori server. </p>
<form id="form-kontak"> <label for="nama">Nama</label> <input id="nama" name="nama" type="text" maxlength="80" autocomplete="name" required >
<label for="email">Email</label> <input id="email" name="email" type="email" maxlength="254" autocomplete="email" required >
<label for="pesan">Pesan</label> <textarea id="pesan" name="pesan" rows="5" maxlength="200" required ></textarea>
<p id="hitung-karakter">0 / 200 karakter</p>
<button id="tombol-kirim" type="submit"> Kirim Pesan </button> </form>
<p id="status-form" role="status" aria-live="polite" ></p> </section>
<section class="kartu" aria-labelledby="judul-status"> <h2 id="judul-status">Status Server</h2>
<p id="status-server">Memuat status...</p>
<button id="tombol-refresh" type="button"> Perbarui Status </button> </section> </main></body></html>Halaman tersebut memiliki tiga bagian utama:
- Profil dan interaksi sederhana.
- Formulir kontak.
- Status server.
Perhatikan bahwa formulir tidak menggunakan atribut action untuk mengirim data secara langsung.
Kita akan menangani event submit menggunakan JavaScript, kemudian mengirim request melalui Fetch API.
12. Membuat CSS dengan Tema Terang dan Gelap
Sekarang buka public/style.css.
Masukkan kode berikut:
:root { --warna-latar: #f3f4f6; --warna-kartu: #ffffff; --warna-teks: #111827; --warna-border: #d1d5db; --warna-tombol: #2563eb; --warna-tombol-teks: #ffffff;}
body.tema-gelap { --warna-latar: #111827; --warna-kartu: #1f2937; --warna-teks: #f9fafb; --warna-border: #4b5563; --warna-tombol: #60a5fa; --warna-tombol-teks: #111827;}
* { box-sizing: border-box;}
body { margin: 0; background: var(--warna-latar); color: var(--warna-teks); font-family: Arial, sans-serif; line-height: 1.6;}
.container { width: min(100% - 32px, 760px); margin: 32px auto;}
.kartu { background: var(--warna-kartu); border: 1px solid var(--warna-border); border-radius: 12px; padding: 24px; margin-bottom: 20px;}
.baris-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; flex-wrap: wrap;}
h1,h2 { margin-top: 0;}
label { display: block; margin: 14px 0 6px; font-weight: bold;}
input,textarea { display: block; width: 100%; padding: 10px; border: 1px solid var(--warna-border); border-radius: 6px; background: var(--warna-kartu); color: var(--warna-teks); font: inherit;}
button { padding: 10px 16px; border: none; border-radius: 6px; background: var(--warna-tombol); color: var(--warna-tombol-teks); cursor: pointer; font: inherit;}
button:disabled { opacity: 0.6; cursor: not-allowed;}
button:focus-visible,input:focus-visible,textarea:focus-visible { outline: 3px solid #f59e0b; outline-offset: 2px;}
#hitung-karakter { font-size: 0.9rem; margin-top: 6px;}
#status-form,#status-server,#hasil-salam { min-height: 24px;}
@media (max-width: 480px) { .container { margin: 16px auto; }
.kartu { padding: 16px; }}CSS ini menggunakan custom properties untuk mengatur warna.
Ketika class tema-gelap ditambahkan ke elemen body, nilai variabel warna berubah.
Dengan begitu, kita tidak perlu menulis ulang seluruh aturan CSS untuk mode gelap.
13. Menyiapkan JavaScript Frontend
Sekarang buka public/script.js.
Kita mulai dengan mengambil elemen HTML.
const tombolTema = document.querySelector("#tombol-tema");const tombolSalam = document.querySelector("#tombol-salam");const hasilSalam = document.querySelector("#hasil-salam");
const tagline = document.querySelector("#tagline");const minat = document.querySelector("#minat");
const formKontak = document.querySelector("#form-kontak");const inputNama = document.querySelector("#nama");const inputEmail = document.querySelector("#email");const inputPesan = document.querySelector("#pesan");
const hitungKarakter = document.querySelector( "#hitung-karakter");
const tombolKirim = document.querySelector( "#tombol-kirim");
const statusForm = document.querySelector("#status-form");const statusServer = document.querySelector("#status-server");const tombolRefresh = document.querySelector( "#tombol-refresh");Kita akan menggunakan elemen-elemen tersebut untuk menghubungkan interaksi pengguna dengan API.
14. Mengambil Data Profil dari Backend
Pada mini project JavaScript sebelumnya, data profil ditulis langsung di HTML.
Sekarang kita akan mengambilnya dari endpoint:
GET /api/profilBuat function:
async function ambilProfil() { try { const response = await fetch("/api/profil");
if (!response.ok) { throw new Error(`HTTP ${response.status}`); }
const data = await response.json();
tagline.textContent = data.tagline;
minat.textContent = `Minat: ${data.minat.join(", ")}`; } catch (error) { tagline.textContent = "Profil belum dapat dimuat."; minat.textContent = "";
console.error(error); }}Ketika halaman dibuka, function ini akan meminta data profil ke backend.
Jika berhasil, informasi profil ditampilkan.
Jika gagal, halaman menampilkan pesan yang mudah dipahami.
15. Membuat Tombol Ganti Tema
Kita pertahankan fitur dari mini project JavaScript sebelumnya.
Ketika tombol tema ditekan, kita menambahkan atau menghapus class tema-gelap.
function gantiTema() { const gelap = document.body.classList.toggle( "tema-gelap" );
tombolTema.textContent = gelap ? "Mode Terang" : "Mode Gelap";
tombolTema.setAttribute( "aria-pressed", String(gelap) );}Fitur ini tidak membutuhkan backend.
Mengganti tema merupakan contoh interaksi yang dapat dilakukan sepenuhnya di browser.
Ini menunjukkan bahwa tidak semua fitur website harus mengirim request ke server.
16. Membuat Tombol Salam
Kita juga pertahankan fitur salam interaktif.
function tampilkanSalam() { hasilSalam.textContent = "Halo, Gabuters! Selamat belajar backend!";}Ketika tombol ditekan, JavaScript memperbarui teks pada halaman.
Fitur ini juga berjalan tanpa backend.
17. Menghitung Karakter Pesan
Formulir kontak membatasi pesan maksimal 200 karakter.
Kita bisa menampilkan jumlah karakter ketika pengguna mengetik.
function perbaruiHitunganKarakter() { hitungKarakter.textContent = `${inputPesan.value.length} / 200 karakter`;}Nilai:
inputPesan.value.lengthDigunakan untuk menghitung panjang string input.
Untuk latihan ini, frontend dan backend sama-sama menggunakan panjang string JavaScript sebagai dasar batas karakter.
Perlu diketahui bahwa panjang string JavaScript tidak selalu sama dengan jumlah karakter yang terlihat oleh manusia, terutama untuk beberapa emoji dan gabungan karakter Unicode.
Pada aplikasi nyata, aturan panjang teks perlu ditentukan sesuai kebutuhan.
18. Mengirim Formulir ke Backend
Sekarang kita masuk ke fitur utama.
Ketika pengguna mengirim formulir, JavaScript akan:
- Mencegah submit HTML biasa.
- Membaca nilai input.
- Melakukan validasi dasar.
- Menonaktifkan tombol sementara.
- Mengirim request
POST. - Membaca response JSON.
- Menampilkan hasil.
- Memulihkan tombol.
Contoh function:
async function kirimPesan(event) { event.preventDefault();
const data = { nama: inputNama.value.trim(), email: inputEmail.value.trim(), pesan: inputPesan.value.trim() };
if (!data.nama || !data.email || !data.pesan) { statusForm.textContent = "Semua field wajib diisi."; return; }
tombolKirim.disabled = true; statusForm.textContent = "Mengirim pesan...";
try { const response = await fetch("/api/pesan", { 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}` ); }
statusForm.textContent = hasil.pesan;
formKontak.reset(); perbaruiHitunganKarakter(); } catch (error) { statusForm.textContent = `Gagal mengirim pesan: ${error.message}`;
console.error(error); } finally { tombolKirim.disabled = false; }}Perhatikan bahwa kita menggunakan:
event.preventDefault();Untuk mencegah browser melakukan submit formulir biasa.
Kemudian:
fetch("/api/pesan", { method: "POST"});Untuk mengirim data ke backend.
Jika backend mengembalikan status 201, kita menampilkan pesan keberhasilan.
Jika backend mengembalikan error, kita menampilkan pesan yang sesuai.
19. Membuat Fitur Status Server
Sekarang kita ingin mengetahui berapa banyak pesan yang sudah diterima server.
Kita menggunakan endpoint:
GET /api/statusBuat function:
async function ambilStatus() { tombolRefresh.disabled = true; statusServer.textContent = "Memuat status...";
try { const response = await fetch("/api/status");
if (!response.ok) { throw new Error(`HTTP ${response.status}`); }
const data = await response.json();
statusServer.textContent = `Server aktif. Pesan diterima: ${data.jumlahPesan}`; } catch (error) { statusServer.textContent = "Status server belum dapat dimuat.";
console.error(error); } finally { tombolRefresh.disabled = false; }}Setelah pesan berhasil dikirim, kita akan memanggil ambilStatus() agar jumlah pesan diperbarui.
20. Membuat JavaScript Frontend Lengkap
Sekarang gabungkan seluruh kode frontend.
Ganti isi public/script.js dengan kode berikut:
const tombolTema = document.querySelector("#tombol-tema");const tombolSalam = document.querySelector("#tombol-salam");const hasilSalam = document.querySelector("#hasil-salam");
const tagline = document.querySelector("#tagline");const minat = document.querySelector("#minat");
const formKontak = document.querySelector("#form-kontak");const inputNama = document.querySelector("#nama");const inputEmail = document.querySelector("#email");const inputPesan = document.querySelector("#pesan");
const hitungKarakter = document.querySelector( "#hitung-karakter");
const tombolKirim = document.querySelector( "#tombol-kirim");
const statusForm = document.querySelector("#status-form");const statusServer = document.querySelector("#status-server");const tombolRefresh = document.querySelector( "#tombol-refresh");
async function ambilProfil() { try { const response = await fetch("/api/profil");
if (!response.ok) { throw new Error(`HTTP ${response.status}`); }
const data = await response.json();
tagline.textContent = data.tagline;
minat.textContent = `Minat: ${data.minat.join(", ")}`; } catch (error) { tagline.textContent = "Profil belum dapat dimuat."; minat.textContent = "";
console.error(error); }}
function gantiTema() { const gelap = document.body.classList.toggle( "tema-gelap" );
tombolTema.textContent = gelap ? "Mode Terang" : "Mode Gelap";
tombolTema.setAttribute( "aria-pressed", String(gelap) );}
function tampilkanSalam() { hasilSalam.textContent = "Halo, Gabuters! Selamat belajar backend!";}
function perbaruiHitunganKarakter() { hitungKarakter.textContent = `${inputPesan.value.length} / 200 karakter`;}
async function ambilStatus() { tombolRefresh.disabled = true; statusServer.textContent = "Memuat status...";
try { const response = await fetch("/api/status");
if (!response.ok) { throw new Error(`HTTP ${response.status}`); }
const data = await response.json();
statusServer.textContent = `Server aktif. Pesan diterima: ${data.jumlahPesan}`; } catch (error) { statusServer.textContent = "Status server belum dapat dimuat.";
console.error(error); } finally { tombolRefresh.disabled = false; }}
async function kirimPesan(event) { event.preventDefault();
const data = { nama: inputNama.value.trim(), email: inputEmail.value.trim(), pesan: inputPesan.value.trim() };
if (!data.nama || !data.email || !data.pesan) { statusForm.textContent = "Semua field wajib diisi."; return; }
tombolKirim.disabled = true; statusForm.textContent = "Mengirim pesan...";
try { const response = await fetch("/api/pesan", { 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}` ); }
statusForm.textContent = `Pesan berhasil diterima dengan ID ${hasil.id}.`;
formKontak.reset(); perbaruiHitunganKarakter();
await ambilStatus(); } catch (error) { statusForm.textContent = `Gagal mengirim pesan: ${error.message}`;
console.error(error); } finally { tombolKirim.disabled = false; }}
tombolTema.addEventListener("click", gantiTema);
tombolSalam.addEventListener("click", tampilkanSalam);
inputPesan.addEventListener( "input", perbaruiHitunganKarakter);
formKontak.addEventListener("submit", kirimPesan);
tombolRefresh.addEventListener("click", ambilStatus);
perbaruiHitunganKarakter();ambilProfil();ambilStatus();Dengan kode ini, seluruh fitur halaman sudah terhubung.
Fitur tema dan salam diproses langsung di browser.
Sedangkan data profil, pengiriman pesan, dan status server menggunakan HTTP request.
21. Menjalankan Mini Project
Pastikan struktur proyek sesuai:
mini-project-api-profil/├── server.js└── public/ ├── index.html ├── style.css └── script.jsBuka terminal pada folder proyek.
Jalankan:
node server.jsJika berhasil, akan muncul:
Server berjalan di http://localhost:3000Buka browser:
http://localhost:3000Halaman profil akan muncul.
Jika API berhasil diakses, tagline dan minat akan diambil dari backend.
Coba tekan tombol Mode Gelap.
Kemudian tekan tombol Sapa Saya.
Kedua fitur tersebut seharusnya bekerja tanpa request API tambahan.
Sekarang coba kirim pesan melalui formulir.
Contohnya:
Nama: GabutersEmail: gabuters@example.comPesan: Halo, saya sedang belajar backend.Tekan tombol Kirim Pesan.
Jika berhasil, frontend menampilkan pesan bahwa backend menerima data.
Jumlah pesan pada bagian Status Server juga akan bertambah.
22. Memeriksa Request Menggunakan Developer Tools
Buka Developer Tools browser.
Pilih tab Network.
Muat ulang halaman.
Kita akan melihat request menuju:
GET /api/profilGET /api/statusSekarang kirim formulir kontak.
Cari request:
POST /api/pesanPerhatikan request payload.
Contohnya:
{ "nama": "Gabuters", "email": "gabuters@example.com", "pesan": "Halo, saya sedang belajar backend."}Kemudian lihat response backend.
Jika berhasil:
HTTP/1.1 201 CreatedDengan body seperti:
{ "berhasil": true, "pesan": "Pesan berhasil diterima.", "id": 1}Setelah itu, frontend mengirim request ke /api/status untuk memperbarui jumlah pesan.
Di sinilah kita bisa melihat seluruh alur frontend dan backend bekerja bersama.
23. Menguji Validasi Backend
Validasi frontend membantu pengguna, tetapi backend tetap harus memeriksa data.
Kita bisa mengujinya menggunakan curl.
Untuk contoh yang mudah dijalankan pada berbagai sistem operasi, buat file:
pesan-tidak-valid.jsonIsi dengan:
{ "nama": "", "email": "bukan-email", "pesan": ""}Kemudian jalankan:
curl -i -X POST http://localhost:3000/api/pesan \ -H "Content-Type: application/json" \ --data-binary "@pesan-tidak-valid.json"Pada Windows PowerShell, gunakan satu baris:
curl.exe -i -X POST http://localhost:3000/api/pesan -H "Content-Type: application/json" --data-binary "@pesan-tidak-valid.json"Server seharusnya mengembalikan:
400 Bad RequestDengan pesan validasi.
Coba perbaiki nama, lalu gunakan email tidak valid.
Server tetap harus menolak request.
Ini menunjukkan bahwa validasi backend tidak bergantung pada formulir HTML.
24. Menguji Status Server
Kita dapat mengakses endpoint:
http://localhost:3000/api/statusContoh response sebelum ada pesan:
{ "status": "ok", "jumlahPesan": 0}Setelah menerima dua pesan valid:
{ "status": "ok", "jumlahPesan": 2}Namun, jika server dihentikan lalu dijalankan kembali, jumlah pesan akan kembali menjadi nol.
Mengapa?
Karena array pesanMasuk dibuat ulang ketika proses Node.js dimulai.
Data tidak disimpan ke file atau database.
25. Memahami Batasan Mini Project
Walaupun mini project ini sudah dapat mengirim pesan ke backend, ada beberapa hal yang perlu diperhatikan.
Pesan belum disimpan permanen
Semua pesan berada di memori.
Jika server restart, data hilang.
Pesan belum dikirim melalui email
Backend hanya menerima dan menyimpan pesan sementara.
Tidak ada integrasi layanan email.
Belum ada autentikasi
Kita tidak membuat sistem login.
Karena itu, kita juga tidak menyediakan endpoint untuk membaca seluruh isi pesan.
Belum ada perlindungan spam
Endpoint POST /api/pesan belum memiliki rate limiting atau mekanisme anti-spam.
Belum siap untuk deployment publik
Aplikasi ini belum mencakup seluruh kebutuhan keamanan, monitoring, dan operasional.
Gunakan untuk latihan lokal.
Jika ingin membuat formulir kontak sungguhan, kita perlu menambahkan penyimpanan permanen, pembatasan request, perlindungan data pribadi, dan mekanisme operasional yang sesuai.
26. Kesalahan yang Sering Terjadi
Server belum dijalankan
Jika browser tidak dapat membuka halaman, pastikan Node.js sudah berjalan.
Gunakan:
node server.jsPort sudah digunakan
Jika muncul error EADDRINUSE, port yang digunakan kemungkinan sudah dipakai proses lain.
Hentikan server sebelumnya atau ubah port.
File HTML dibuka menggunakan file://
Jangan membuka file HTML langsung melalui filesystem.
Buka halaman menggunakan:
http://localhost:3000Agar request relatif menuju origin server yang benar.
Formulir tidak mengirim request
Periksa apakah script.js berhasil dimuat.
Buka tab Console pada Developer Tools untuk melihat error JavaScript.
Pastikan event listener dipasang pada elemen yang sesuai.
Server mengembalikan 415
Status 415 Unsupported Media Type dapat muncul jika request tidak menggunakan:
Content-Type: application/jsonPastikan Fetch API mengirim header yang sesuai.
Server mengembalikan 400
Periksa format JSON dan aturan validasi.
Pastikan nama, email, dan pesan memenuhi batas yang ditentukan.
Jumlah pesan kembali menjadi nol
Ini merupakan perilaku yang diharapkan karena data hanya berada di memori.
Kita belum menggunakan database.
Menganggap pesan sudah terkirim melalui email
Pada proyek ini, respons berhasil hanya berarti backend menerima dan menyimpan pesan sementara di memori.
Tidak ada email yang dikirim.
27. Latihan Mandiri
Sekarang coba kembangkan mini project ini.
Latihan 1: Menambahkan field subjek
Tambahkan input:
SubjekKe formulir kontak.
Perbarui validasi frontend dan backend.
Batasi subjek maksimal 100 karakter.
Latihan 2: Menambahkan waktu server
Buat endpoint:
GET /api/waktuYang mengembalikan:
{ "waktuServer": "2026-10-09T12:30:00.000Z"}Nilai waktu harus dihasilkan secara dinamis menggunakan:
new Date().toISOString()Latihan 3: Menampilkan pesan ketika server tidak tersedia
Jalankan halaman terlebih dahulu.
Kemudian hentikan proses Node.js.
Coba perbarui status server.
Pastikan frontend menampilkan pesan error yang mudah dipahami.
Latihan 4: Menambahkan validasi panjang nama
Di frontend, tampilkan pesan jika nama melebihi 80 karakter.
Pastikan backend tetap melakukan validasi yang sama.
Latihan 5: Membuat endpoint health check
Tambahkan:
GET /healthYang mengembalikan:
{ "status": "ok"}Endpoint ini dapat digunakan sebagai pemeriksaan dasar apakah proses server masih merespons request.
Namun, endpoint sederhana tersebut belum membuktikan bahwa seluruh fitur dan dependency aplikasi bekerja dengan baik.
28. Apa yang Sudah Kita Pelajari?
Melalui mini project ini, kita berhasil menggabungkan konsep dari beberapa seri pembelajaran.
Dari HTML, kita menggunakan struktur halaman dan formulir.
Dari CSS, kita membuat tampilan responsif serta tema terang dan gelap.
Dari JavaScript browser, kita menggunakan DOM, event, dan validasi input.
Dari materi backend, kita menggunakan:
- Node.js native.
- Modul
node:http. - HTTP request dan response.
- Endpoint API.
- JSON.
- Fetch API.
- Promise dan
async/await. - Validasi backend.
- Status code.
- Penanganan error.
- Penyimpanan sementara di memori.
Sekarang kita sudah mempunyai gambaran bagaimana sebuah fitur website bekerja dari browser hingga backend.
29. Penutup: Seri Backend dan API Selesai!
Selamat, Gabuters!
Kita sudah menyelesaikan seri Backend dan API dengan JavaScript.
Perjalanan kita dimulai dari memahami konsep backend, server, dan API.
Kemudian kita belajar HTTP, JSON, dan REST API.
Setelah itu, kita membuat HTTP server sendiri menggunakan Node.js native tanpa framework.
Kita melanjutkannya dengan operasi CRUD, membaca request body, dan memvalidasi data.
Kita juga belajar menghubungkan frontend ke backend menggunakan Fetch API.
Dan akhirnya, pada mini project ini, kita berhasil membuat formulir kontak yang benar-benar mengirim request ke backend.
Namun, masih ada satu keterbatasan besar.
Data kita belum tersimpan secara permanen.
Setiap kali proses server dihentikan, pesan yang disimpan di memori akan hilang.
Bagaimana agar data tetap tersedia setelah server restart?
Bagaimana cara menyimpan, mencari, memperbarui, dan menghapus data secara terstruktur?
Untuk menjawabnya, kita akan melanjutkan perjalanan ke seri berikutnya: Database untuk Pemula.
Di sana, kita akan mengenal database relasional, SQL, tabel, primary key, dan cara menghubungkan backend dengan database.
Sampai jumpa di seri berikutnya, Gabuters!
Bagikan Artikel
Buat poster berbagi atau salin tautan artikel ini.
Lanjut membaca
Coba jalur lain
Pilihan konsisten dari artikel lain
Terakhir diperbarui pada , 21 hari yang lalu
Sebagian konten mungkin sudah tidak berlaku