Membangun Aplikasi Interaktif dengan htmx: Interaktivitas Modern tanpa JavaScript Framework
Interaktivitas Sering Dibangun dengan Framework Padahal Server Sudah Cukup
Banyak fitur interaktif yang kita bangun sebenarnya hanya membutuhkan pertukaran fragmen HTML: tab yang berpindah konten, dropdown bertingkat, infinite scroll, atau submit form tanpa reload halaman. Namun pola umum di industri membungkus kebutuhan sederhana tersebut dalam bundel JavaScript framework yang besar, lengkap dengan build pipeline dan runtime client.
Biaya dari pendekatan tersebut muncul perlahan. Kita membayar hydration runtime yang harus menjalankan ulang markup di browser, menambah lapisan state management baru, dan pada akhirnya men-debug dua sistem sekaligus: client state yang rumit diprediksi dan server state yang sudah menjadi sumber kebenaran. Setiap fitur baru menuntut sinkronisasi antara keduanya agar UI tetap konsisten.
htmx.org menawarkan jalur alternatif. Library ini berukuran sekitar ~13KB dan memperluas kemampuan HTML melalui atribut biasa. Kita cukup menandai elemen dengan atribut seperti hx-get atau hx-post, lalu htmx mengirim request ke server dan menempatkan fragmen HTML balasan ke bagian DOM yang ditentukan.
Sepanjang artikel ini, kita membangun satu contoh fitur end-to-end: daftar produk dengan pencarian langsung, pagination, dan form tambah data. Server menggunakan Express dengan template EJS, dan setiap interaksi tetap diproses di sisi server sebagai alur kerja yang dapat kita baca dan uji dengan jelas.
Cara Kerja Atribut htmx sebagai Lapisan Hypermedia
Atribut inti yang perlu kita kuasai adalah hx-get, hx-post, hx-target, dan hx-swap. Atribut hx-get dan hx-post menentukan metode dan URL request, hx-target memilih elemen DOM yang akan diperbarui, dan hx-swap menentukan bagaimana hasilnya disisipkan, misalnya innerHTML, beforeend, atau outerHTML. Di balik layar, htmx mengirim XMLHttpRequest seperti request fetch biasa, lalu mengganti bagian DOM dengan respons HTML yang diterima.
Server dapat membedakan request htmx dari request biasa melalui header HX-Request. Ketika header tersebut ada, server merender fragmen parsial; ketika tidak ada, server mengirim halaman penuh. Header HX-Trigger bekerja ke arah sebaliknya, memungkinkan server memicu event di client tanpa menulis kode JavaScript tambahan.
Contoh halaman minimal berikut memperlihatkan alur request menuju swap dengan satu tombol dan satu target.
<!DOCTYPE html>
<html lang="id">
<head>
<title>htmx Demo</title>
<script src="https://unpkg.com/[email protected]"></script>
</head>
<body>
<button hx-get="/hello" hx-target="#result" hx-swap="innerHTML">
Muat konten
</button>
<div id="result"></div>
</body>
</html>Ketika tombol ditekan, htmx mengirim GET /hello, menerima HTML balasan dari server, lalu menempatkannya ke dalam #result. Client tidak pernah membangun UI dari data JSON, karena server mengirim HTML final yang siap ditampilkan. Konsep ini disebut hypermedia-driven: respons server adalah state aplikasi, dan hypermedia sendiri pada dasarnya adalah gagasan bahwa server mengirim representasi lengkap sumber daya beserta tautan aksi berikutnya, bukan data mentah yang harus dirender ulang oleh client.

Gambar: Diagram sekuens model client/server pada web browsing, memperlihatkan alur request dari browser menuju server dan respons HTML yang dikembalikan ke client — Sumber: Wikimedia Commons_-_en.png)
Menyiapkan Server Node.js dan Endpoint HTML Parsial Pertama
Struktur proyek yang kita gunakan sengaja dijaga minimal: server.js sebagai entry point, folder views/ berisi template EJS, dan folder public/ untuk aset statis. Pemisahan ini penting karena partial yang sama akan dirender baik untuk request htmx maupun sebagai bagian dari layout penuh.
Kita memulai dengan dependensi dasar dan satu route produk.
Advanced Architecture with Laravel Containers & Queues
A problem-driven, project-based course designed to elevate intermediate develope...
const express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.urlencoded({ extended: true }));
app.use(express.static('public'));
const products = [
{ id: 1, name: 'Mechanical Keyboard', price: 850000 },
{ id: 2, name: 'Wireless Mouse', price: 320000 }
];
app.get('/products', (req, res) => {
const isHtmx = req.get('HX-Request') === 'true';
const q = (req.query.q || '').toLowerCase();
const filtered = products.filter((p) => p.name.toLowerCase().includes(q));
if (isHtmx) {
return res.render('_product-rows', { products: filtered });
}
res.render('index', { products: filtered });
});
app.listen(3000, () => console.log('Server jalan di http://localhost:3000'));Output:
# GET http://localhost:3000/products (request biasa)
<!DOCTYPE html>
<html lang="id">
<head>
<title>Daftar Produk</title>
...
</head>
<body>
<h1>Daftar Produk</h1>
...
<table id="product-table">
<tbody>
<tr data-id="1">
<td>Mechanical Keyboard</td>
<td>850000</td>
</tr>
<tr data-id="2">
<td>Wireless Mouse</td>
<td>320000</td>
</tr>
</tbody>
</table>
</body>
</html>
# GET http://localhost:3000/products (HX-Request: true)
<tr data-id="1">
<td>Mechanical Keyboard</td>
<td>850000</td>
</tr>
<tr data-id="2">
<td>Wireless Mouse</td>
<td>320000</td>
</tr>Poin penting dari route tersebut adalah percabangan berdasarkan header HX-Request. Untuk request htmx, server hanya merender _product-rows.ejs, yaitu fragmen baris tabel tanpa elemen <html> atau <body>. Untuk request normal, server merender index.ejs secara penuh sehingga halaman tetap dapat diakses langsung. Karena server mengirim HTML parsial, client tidak memerlukan logic rendering sama sekali.

Gambar: Perbandingan alur aplikasi web konvensional yang memuat ulang halaman penuh melawan pendekatan parsial yang hanya memperbarui sebagian DOM — Sumber: Wikimedia Commons
File views/_product-rows.ejs hanya berisi baris tabel yang datanya berasal dari variabel products. Setelah menjalankan node server.js, browser yang membuka http://localhost:3000/products menerima halaman penuh, sementara htmx menerima fragmen yang siap disisipkan. Perbedaan respons ini dapat kita periksa langsung melalui tab Network di DevTools.
Pencarian, Trigger, dan Debounce dengan Atribut hx-trigger
Menambahkan pencarian langsung tidak memerlukan event listener manual. Kita cukup menempelkan atribut htmx pada elemen input dan membiarkan hx-trigger mengatur kapan request dikirim.
<input
type="search"
name="q"
placeholder="Cari produk..."
hx-get="/products"
hx-trigger="keyup changed delay:300ms, search"
hx-target="#product-table tbody"
hx-swap="innerHTML"
/>Nilai keyup changed delay:300ms meniru perilaku debounce tanpa kode JavaScript. Setiap penekanan tombol tidak langsung menghasilkan request, karena htmx menunggu 300ms setelah input terakhir berubah sebelum mengirim GET /products?q=.... Modifier changed memastikan request hanya dikirim ketika nilai benar-benar berbeda, sehingga penekanan panah atau tombol Shift tidak memicu request yang tidak perlu.
Ketika kita perlu menyertakan parameter dari elemen lain, atribut hx-include dapat merujuk selector tertentu, sementara hx-vals menambahkan nilai statis. Alur kerjanya sederhana: pengguna mengetik, htmx menunggu jeda 300ms, mengirim request dengan query q, lalu server merender ulang _product-rows.ejs berdasarkan hasil filter. Karena penggantian hanya menyasar <tbody>, posisi scrollbar dan fokus input tetap stabil.
Kita juga perlu menangani empty state agar pencarian tanpa hasil tidak menyisakan tabel kosong tanpa penjelasan. Cukup tambahkan baris pesan pada partial ketika array products kosong, dan fokus tetap berada di input sehingga pengguna dapat melanjutkan pengetikan tanpa gangguan aksesibilitas.
Form Tanpa Reload dan Respons Error dari Server
Form tambah produk memakai pendekatan yang sama, hanya saja metodenya hx-post dan hasil suksesnya disisipkan ke akhir tabel.
<form
hx-post="/products"
hx-target="#product-table tbody"
hx-swap="beforeend"
hx-disabled-elt="button[type=submit]"
>
<label for="name">Nama produk</label>
<input id="name" name="name" type="text" required />
<button type="submit">Tambah</button>
</form>Route POST /products memvalidasi input, menambahkan produk ke daftar ketika valid, lalu merender ulang partial sehingga baris baru muncul di bagian bawah tabel. Ketika validasi gagal, server tidak mengembalikan status sukses kosong, melainkan merender ulang form dengan pesan error per field. htmx kemudian mengganti bagian form melalui hx-target yang diarahkan ke form atau container error, sehingga pesan validasi muncul tanpa reload halaman.
Untuk kasus yang memang membutuhkan perpindahan halaman, misalnya setelah login, server dapat mengirim header HX-Redirect agar browser berpindah secara otomatis. Sementara itu, hx-disabled-elt menonaktifkan tombol submit selama request berjalan, sehingga mencegah pengiriman ganda ketika koneksi lambat.
Hasil yang kita harapkan: baris produk bertambah segera setelah submit berhasil, pesan validasi muncul di samping field yang bermasalah tanpa reload, dan tombol submit kembali aktif begitu request selesai. Semua logika validasi tetap berada di server, sehingga aturan bisnis tidak terduplikasi di dua tempat.
Batasan htmx dan Praktik Terbaik Sebelum Dipakai di Produksi
htmx paling tepat untuk aplikasi server-rendered seperti CRUD, dashboard internal, dan proyek yang sudah memakai template engine. Pada konteks tersebut, penggantian fragmen HTML menghilangkan kebutuhan menyinkronkan state client dengan server, dan kode menjadi lebih sedikit secara keseluruhan.
Beberapa kasus tetap lebih baik ditangani JavaScript murni. Interaksi lokal yang kompleks seperti drag pada canvas atau editor berbasis keyboard, aplikasi offline-first dengan sinkronisasi konflik, serta animasi bertingkat yang bergantung pada banyak state client, semuanya menuntut kontrol DOM yang detail di sisi client. Untuk kebutuhan seperti ini, framework JavaScript tetap menjadi alat yang tepat.
Sebelum membawa htmx ke produksi, kita perlu menjaga beberapa praktik. Sediakan fallback untuk request non-JavaScript agar halaman tetap berfungsi, jaga ukuran partial tetap kecil supaya payload responsif, dan pantau setiap request melalui DevTools Network untuk memastikan tidak ada pengiriman fragmen yang berlebihan. Konsistensi penamaan atribut hx-* di seluruh proyek juga membantu tim membaca alur interaksi dengan cepat.
Keputusan akhirnya bersifat pragmatis: mulai dengan pendekatan hypermedia, lalu tambahkan JavaScript hanya pada bagian yang benar-benar membutuhkan state client. Dengan cara ini, arsitektur tetap sederhana selama kebutuhan masih dapat dipenuhi oleh HTML dari server.
Mau menguasai pola server-rendered dan hypermedia secara terstruktur? Bergabunglah dengan Web Development Bootcamp di Rumah Coding. Kurikulum praktis dengan proyek real-world, mulai dari Express, template engine, hingga deployment, dibimbing langsung oleh praktisi industri.
Course Terkait
Advanced Architecture with Laravel Containers & Queues
A problem-driven, project-based course designed to elevate intermediate developers to a senior architectural mindset. Instead of just memorizing documentation, students will tackle real-world bottleneck issues by mastering the Inversion of Control (IoC) principle, Dependency Injection, and asynchronous background processing. Learn to decouple services and orchestrate robust queues to build scalable, high-performance applications that never freeze under heavy loads.
Asynchronous Bulk E-Certificate & Notification Engine
- Decoupled Service Architecture: PDF generation and Email delivery are built as independent, interface-driven services injected via the Service Container.
- Job Chaining: Ensuring processes run in strict asynchronous order (Generate PDF ➔ Upload to Storage ➔ Send Email).
- Job Batching & Real-Time Tracking: Grouping hundreds of jobs together and displaying a live progress bar (e.g., "65% Completed") on the admin dashboard.
API Development with Golang
A hands-on, project-based course designed to teach beginners how to build robust, high-performance RESTful APIs using Golang. Starting from core Go syntax and concurrency, students will progress through routing, database integration, security, and finally containerize their application for production deployment.
EduTrack: Learning Management & Assessment API
- Role-Based Access Control (RBAC)
- Course & Module Management
- Enrollment & Progress Tracking
Building Modern Apps with Filament
Learn how to build modern, full-stack web applications rapidly using FilamentPHP and the TALL stack. This project-based course is designed for beginners, guiding you step-by-step to create a fully functional backend administration panel. By the end of the course, you will have mastered Filament's powerful Panel Builder, robust CRUD operations, complex database relationships, and interactive dashboards.
Mini Academic Portal (Learning Management System)
- Master Data Management: Comprehensive CRUD functionality to manage Instructors and Course catalogs.
- Student Registry: A dedicated module to manage student profiles and personal information.
- Dynamic Enrollment System: Handling Many-to-Many relationships to enroll students into specific courses using Filament's pivot features.