Chapter sebelumnya memisahkan data, logika, dan tampilan ke dalam function masing-masing, tetapi semuanya masih hidup dalam satu berkas. Begitu aplikasi tumbuh, berkas itu dipecah menjadi banyak module — dan muncul pertanyaan baru: satu module sebaiknya berisi apa, dan diletakkan di mana? Jawaban yang asal-asalan menghasilkan proyek dengan puluhan berkas yang tidak ada yang paham susunannya, termasuk kita sendiri enam bulan kemudian.
Modular architecture adalah kebiasaan menyusun aplikasi dari module-module kecil yang masing-masing punya satu tanggung jawab yang jelas, lalu mengelompokkannya menurut fitur. Chapter ini membahas dua keputusan intinya — pembagian tanggung jawab dan penataan folder — lalu mempraktikkannya dengan fitur perpustakaan yang tersusun dari empat berkas module.
Satu Module, Satu Tanggung Jawab #
Aturannya sederhana: satu module hanya punya satu alasan untuk berubah. Module katalog.mjs berubah karena daftar buku berubah; module sirkulasi.mjs berubah karena aturan penyewaan berubah. Keduanya tidak perlu berubah bersamaan, maka keduanya tidak disatukan.
Uji praktisnya bisa dilakukan dengan satu kalimat: “Module ini bertanggung jawab atas apa?” Bila jawabannya butuh kata “dan”, isinya kemungkinan besar layak dipecah. katalog.mjs menjawab “menyimpan dan mencari buku” — masih satu bidang karena menyimpan dan mencari adalah dua wajah dari data yang sama. Sebaliknya, “menyimpan buku dan menghitung denda” adalah dua bidang yang berbeda alasannya, maka keduanya berhak jadi dua module.
Keterampilan yang melatih kebiasaan ini adalah pemberian nama. Nama module yang baik menyebutkan tanggung jawabnya, bukan jenis isinya: sirkulasi.mjs lebih bermakna daripada util.mjs, dan katalog.mjs lebih jelas daripada data1.mjs. Nama yang tepat membuat pembaca bisa menebak isi berkas tanpa membukanya.
Tiga tanda bahwa sebuah module perlu dipecah biasanya muncul bersamaan. Pertama, isinya memanjang karena menampung dua hal yang tidak berubah bersama — data katalog bertambah, sementara aturan denda jarang disentuh. Kedua, pengimpornya terbagi dua kelompok dengan kebutuhan berbeda: yang satu hanya butuh data, yang lain hanya butuh aturan. Ketiga, setiap kali module itu disunting, konflik penggabungan (merge conflict) muncul lebih sering karena dua orang mengedit dua kepentingan berbeda di berkas yang sama. Ketiga tandanya mengarah pada solusi yang sama: pisahkan apa yang tidak berubah bersama.
Menata Folder: By Feature atau By Type #
Setelah module-module jelas tanggung jawabnya, keputusan berikutnya adalah penempatan. Dua pendekatan yang paling umum: mengelompokkan berdasarkan jenis berkas (by type) atau berdasarkan fitur (by feature). Susunan by type mengumpulkan semua berkas dengan jenis yang sama:
src/
├── data/
│ ├── katalog.mjs
│ └── anggota.mjs
├── logika/
│ ├── sewa.mjs
│ └── denda.mjs
└── tampilan/
└── laporan.mjsSusunan by feature mengumpulkan semua berkas milik satu fitur dalam satu folder:
src/
├── perpustakaan/
│ ├── katalog.mjs ← data fitur
│ ├── sirkulasi.mjs ← logika fitur
│ └── index.mjs ← barrel fitur
├── anggota/
│ ├── profil.mjs
│ ├── kartu.mjs
│ └── index.mjs
└── utama.mjs ← titik masuk aplikasiKeduanya sah, tetapi konsekuensinya berbeda:
- By type mudah dipahami di awal (“semua data di satu tempat”), tetapi satu fitur tersebar di tiga folder. Mengubah fitur perpustakaan berarti membuka tiga folder berbeda, dan menghapus fitur berarti menyisir semua folder.
- By feature menempatkan apa yang berubah bersama di tempat yang berdekatan. Menghapus atau memindahkan satu fitur cukup memindahkan satu folder, dan folder fitur langsung memperlihatkan peta aplikasinya.
Proyek kecil dengan sepuluh berkas lazim memakai by type tanpa masalah. Begitu jumlah fitur bertambah, by feature lebih mudah dirawat karena keberatan (concern) yang dijaganya adalah fitur itu sendiri — persis prinsip chapter sebelumnya, dinaikkan ke tingkat folder.
Contoh: Fitur Perpustakaan dalam Empat Berkas #
Contoh berikut memakai susunan by feature dengan dua module isi plus barrel dan titik masuk. Module pertama memegang data:
// katalog.mjs — module data: menyimpan dan mencari buku.
// Tidak ada penghitungan biaya dan tidak ada pencetakan di sini.
export const katalog = [
{ kode: "JS-01", judul: "Dasar JavaScript", kategori: "pemrograman", tarif: 3000 },
{ kode: "JS-02", judul: "Struktur Data", kategori: "pemrograman", tarif: 3000 },
{ kode: "SR-07", judul: "Sejarah Nusantara", kategori: "umum", tarif: 2000 },
];
export function cariBuku(kode) {
return katalog.find((buku) => buku.kode === kode) ?? null;
}Module kedua memegang logika penyewaan:
// sirkulasi.mjs — module logika: aturan penyewaan buku.
// Menerima data dari luar, mengembalikan hasil; tidak menyimpan data sendiri.
export function hitungSewa(buku, hari) {
const biaya = hari <= 0 ? 0 : buku.tarif * hari;
return { judul: buku.judul, hari, biaya };
}Barrel merangkum keduanya menjadi satu pintu:
// index.mjs — barrel: pintu masuk tunggal fitur perpustakaan.
export { katalog, cariBuku } from "./katalog.mjs";
export { hitungSewa } from "./sirkulasi.mjs";Terakhir, titik masuk menyambung semuanya menjadi satu alur:
// utama.mjs — titik masuk: menyambung data dan logika menjadi satu alur.
// Semua kebutuhan diambil lewat barrel, bukan dari berkas di dalam folder.
import { cariBuku, hitungSewa } from "./index.mjs";
const buku = cariBuku("JS-01");
console.log(buku.judul);
const sewa = hitungSewa(buku, 3);
console.log(`${sewa.judul} - ${sewa.hari} hari: Rp${sewa.biaya}`);
const tidakAda = cariBuku("XX-99");
console.log(tidakAda);Jalankan dari folder chapter ini di terminal:
node utama.mjsOutput yang Dihasilkan:
Dasar JavaScript
Dasar JavaScript - 3 hari: Rp9000
nullMari kita telaah baris-baris pentingnya:
- Baris 11
katalog.mjs(return katalog.find(...) ?? null;):cariBukumengembalikannulluntuk kode yang tidak terdaftar — keputusan kecil yang penting, karena pemanggil memperoleh jawaban yang pasti, bukanundefinedyang menyelinap. - Baris 5
sirkulasi.mjs(const biaya = hari <= 0 ? 0 : buku.tarif * hari;): logika menangani kasus batas sendiri tanpa memercayai input pemanggil, dan ia tidak pernah membacakataloglangsung — datanya datang lewat parameter, sehingga module logika bisa diuji dengan buku apa pun. - Baris 2–3
index.mjs(export { ... } from ...): barrel hanya me-re-export; ia tidak memegang logika. Detail bentuk barrel sudah dibahas di chapter module organization — di sini cukup dipakai sebagai kebiasaan. - Baris 4
utama.mjs(import { cariBuku, hitungSewa } from "./index.mjs";): titik masuk mengimpor dari barrel, bukan darikatalog.mjsatausirkulasi.mjslangsung. Bila suatu harisirkulasi.mjsdipecah menjadi dua berkas,utama.mjstidak perlu berubah. - Baris 12–13
utama.mjs(const tidakAda = cariBuku("XX-99"); ...): keluarannullmembuktikan jalur tidak-terdaftar bekerja — percobaan kecil seperti ini adalah cara paling murah memastikan kontrak antar module benar-benar dipatuhi.
Arah Ketergantungan Tetap Searah #
Pada contoh di atas, arah pergantian jelas: utama.mjs bergantung pada barrel, barrel bergantung pada katalog.mjs dan sirkulasi.mjs, dan kedua module itu tidak mengimpor siapa pun di proyeknya sendiri. Tidak ada yang mengimpor ke atas.
Arah satu-arah ini patut dijaga sejak proyek masih kecil. Bila sirkulasi.mjs suatu saat mengimpor utama.mjs — misalnya untuk memakai data titik masuk — kedua berkas saling menunggu, dan kita kembali pada masalah circular dependency yang sudah dibahas di section Modules. Kebiasaan pencegahnya sederhana: module yang lebih dalam tidak boleh tahu keberadaan module di atasnya. Bila ada nilai yang dibutuhkan bersama, nilainya dikirim lewat parameter atau ditaruh di module tersendiri yang posisinya paling bawah.
Kebiasaan ini bisa ditolong alat. Aturan ESLint seperti no-restricted-imports dari section Tooling dan Code Quality bisa melarang pola impor tertentu — misalnya melarang folder fitur mengimpor berkas titik masuk — sehingga pelanggaran arah tertangkap sebelum penggabungan, bukan saat aplikasi mulai bermasalah. Aturan seperti ini hanya efektif bila arah yang benar sudah disepakati, dan itulah sebabnya keputusan di chapter ini didahulukan sebelum aturan alatnya ditulis.
Dengan module yang tegas tanggung jawabnya, pertanyaan berikutnya muncul di tingkat perilaku aplikasi: ketika data berubah — keranjang ditambah, daftar diperbarui — siapa yang menyimpan nilai terkini, dan bagaimana tampilan tahu ia perlu digambar ulang? Pertanyaan itu adalah wilayah state management, yang dibahas di chapter berikutnya.