Aplikasi sungguhan jarang cukup ditampung satu berkas: proyek besar terdiri dari puluhan berkas dengan tanggung jawab berbeda. Node.js sejak 2009 mengatasinya dengan sistem module bawaan bernama CommonJS — setiap berkas JavaScript adalah module dengan ruang lingkup tersendiri, dan berkas lain dapat memuatnya lewat function require(). Meskipun JavaScript kini memiliki standar module yang lebih baru, CommonJS tetap menghidupi jutaan package lama di npm sehingga wajib kita pahami. Chapter ini membahas cara mengekspor kode dengan module.exports, jebakan alias exports, perilaku cache Node.js, serta cara memuat folder sebagai satu module.
Kenapa Kode Perlu Dipecah Menjadi Module #
Bayangkan satu berkas berisi 10.000 baris: mencari satu function menjadi lambat, dua variable bisa tanpa sengaja memakai nama sama, dan bekerja berpasangan hampir mustahil karena semua orang menyunting berkas yang sama. Module menyelesaikan ketiganya sekaligus. Node.js membungkus setiap berkas ke dalam sebuah function pembungkus tersembunyi, sehingga setiap variable dan function tingkat atas bersifat privat — tidak terlihat dari berkas lain kecuali secara sengaja diekspor.
Function pembungkus tersebut menerima lima parameter, dan dua di antaranya adalah bintang utama Chapter ini:
function (exports, require, module, __filename, __dirname) {
// ... isi berkas module kita berada di sini ...
}Parameter module dan exports adalah pintu keluar kode, require adalah pintu masuk kode dari module lain, sementara __filename dan __dirname memberi tahu posisi berkas di disk. Karena pembungkus ini dibuat per berkas, tidak ada nama yang bocor antar berkas — kolisi nama global seperti era <script> berantai di halaman web tidak terjadi lagi.
require dan module.exports #
Module mengekspor nilai dengan memberikannya ke module.exports, lalu module pemakai memuatnya dengan require(). Kita mulai dari module matematika.cjs berikut:
const PI = 3.14;
function tambah(a, b) {
return a + b;
}
function kali(a, b) {
return a * b;
}
module.exports = { PI, tambah, kali };Berkas utama memuat module di atas lalu memakai isinya seperti object biasa:
const matematika = require("./matematika.cjs");
console.log(`2 + 3 = ${matematika.tambah(2, 3)}`);
console.log(`4 x 5 = ${matematika.kali(4, 5)}`);
console.log(`10 + (4 x 5) = ${matematika.tambah(10, matematika.kali(4, 5))}`);Output yang Dihasilkan:
2 + 3 = 5
4 x 5 = 20
10 + (4 x 5) = 30Mari kita telaah baris-baris kode pada kedua berkas di atas:
matematika.cjsbaris 9 (module.exports = { PI, tambah, kali }): object berisi konstanta dan dua function menjadi nilai balikanrequire(). Shorthand{ PI, tambah, kali }sama dengan{ PI: PI, tambah: tambah, kali: kali }.utama.cjsbaris 1 (const matematika = require(...)): functionrequire()menerima specifier — di sini path relatif"./matematika.cjs"— lalu mengembalikan nilaimodule.exportsmilik berkas tersebut. Tanda./wajib; tanpa tanda tersebut Node.js menganggap kita memanggil nama package, bukan path.utama.cjsbaris 3-5: hasilrequire()disimpan pada variablematematika, sehinggamatematika.tambah(2, 3)memanggil function dari module lain seperti memanggil method object. Baris terakhir juga menunjukkan hasilkali()bisa langsung dipakai sebagai argumenttambah().
exports sebagai Alias ke module.exports #
Parameter exports pada function pembungkus awalnya menunjuk object yang sama dengan module.exports. Karena keduanya object yang sama, exports.tambah = ... menempelkan properti ke object yang dikembalikan require(). Namun operator = pada exports = ... hanya mengarahkan ulang variable exports ke object baru — module.exports tetap menunjuk object lama yang kosong. Perbedaan halus ini menghasilkan module kosong tanpa pesan error sama sekali.
Dua module berikut memperlihatkan kedua gaya tersebut:
exports.catatWaktu = function (jam) {
return `kegiatan dicatat pukul ${jam}.00`;
};exports = {
catatWaktu: function (jam) {
return `kegiatan dicatat pukul ${jam}.00`;
},
};Kedua module di atas dimuat oleh satu berkas pembanding:
const benar = require("./benar-alias.cjs");
const salah = require("./salah-alias.cjs");
console.log("lewat exports.properti:", benar.catatWaktu(8));
console.log("isi module yang salah:", JSON.stringify(salah));Output yang Dihasilkan:
lewat exports.properti: kegiatan dicatat pukul 8.00
isi module yang salah: {}benar-alias.cjs berhasil mengekspor catatWaktu, sedangkan salah-alias.cjs mengembalikan object kosong {} — reassignmen exports terjadi tanpa error, dan require() tetap mengambil isi module.exports. Aturan praktisnya sederhana: gunakan exports.properti = nilai untuk menambah satu per satu, atau gunakan module.exports = nilai untuk mengganti seluruh ekspor — jangan pernah mencampur keduanya dengan reassignmen exports.
Module yang Telah Dimuat Disimpan di Cache #
require() tidak mengevaluasi berkas yang sama berulang kali. Saat pertama kali dipanggil untuk satu berkas, Node.js mengevaluasi berkas tersebut, menyimpan hasilnya di cache internal, lalu mengembalikan nilai module.exports. Panggilan require() berikutnya untuk berkas yang sama langsung menerima object yang sama persis dari cache — kode di dalam berkas tidak dieksekusi ulang. Kita buktikan dengan module yang menyimpan status internal:
console.log("pencatat.cjs dievaluasi");
let jumlahCatatan = 0;
function catat(keterangan) {
jumlahCatatan += 1;
return `catatan #${jumlahCatatan}: ${keterangan}`;
}
function totalCatatan() {
return jumlahCatatan;
}
module.exports = { catat, totalCatatan };Berkas berikut memuat module tersebut dua kali:
const pencatatA = require("./pencatat.cjs");
const pencatatB = require("./pencatat.cjs");
console.log("objek sama?", pencatatA === pencatatB);
console.log(pencatatA.catat("beli susu"));
console.log(pencatatB.catat("kirim laporan"));
console.log("total lewat pencatatB:", pencatatB.totalCatatan());Output yang Dihasilkan:
pencatat.cjs dievaluasi
objek sama? true
catatan #1: beli susu
catatan #2: kirim laporan
total lewat pencatatB: 2Teks pencatat.cjs dievaluasi hanya muncul sekali meskipun require() dipanggil dua kali — bukti evaluasi hanya terjadi pada panggilan pertama. Perbandingan pencatatA === pencatatB menghasilkan true karena keduanya menunjuk object yang sama, sehingga jumlahCatatan terus bertambah dari 1 ke 2 tanpa reset. Efek sampingnya, module CommonJS secara alami berperan sebagai singleton: koneksi basis data atau konfigurasi bersama cukup disimpan sekali di satu module, dan seluruh aplikasi memakai instance yang sama.
Memuat Folder sebagai Module #
Saat proyek membesar, satu folder sering menjadi satu kesatuan fitur. CommonJS memungkinkan require() menerima path folder — Node.js lalu mencari berkas index.js di dalam folder tersebut sebagai titik masuk. Kita susun folder utilitas/ berisi satu berkas:
function formatRupiah(angka) {
return `Rp${angka.toLocaleString("id-ID")}`;
}
module.exports = { formatRupiah };Folder tersebut dimuat tanpa menyebut index.js secara eksplisit:
const { formatRupiah } = require("./utilitas");
console.log(formatRupiah(1500000));
console.log(formatRupiah(25500));Output yang Dihasilkan:
Rp1.500.000
Rp25.500Specifier "./utilitas" menunjuk folder, dan Node.js menemukan utilitas/index.js secara otomatis. Pola ini juga dipakai jutaan package npm: folder package memuat index.js atau field main di package.json sebagai pintu masuk — mekanisme lengkapnya kita bedah pada Chapter Resolusi Module. Perhatikan pula destructuring const { formatRupiah } = require(...) — bentuk singkat untuk mengambil satu properti dari object ekspor.
Variabel __dirname dan __filename #
Dua parameter pembungkus terakhir memberi tahu module posisinya di disk: __filename berisi path lengkap berkas, dan __dirname berisi path folder tempat berkas berada. Keduanya berguna untuk membaca berkas data yang disimpan bersebelahan dengan kode, tanpa bergantung pada folder tempat perintah node dijalankan. Agar output tetap pendek, contoh berikut hanya mencetak bagian nama akhirnya:
const path = require("node:path");
console.log("nama berkas :", path.basename(__filename));
console.log("nama folder :", path.basename(__dirname));
console.log("folder kerja proses:", path.basename(process.cwd()));
console.log("__dirname sama dengan folder kerja?", __dirname === process.cwd());Output yang Dihasilkan:
nama berkas : berkas-info.cjs
nama folder : sistem-module-commonjs
folder kerja proses: sistem-module-commonjs
__dirname sama dengan folder kerja? trueBaris pertama memuat core module path lewat specifier "node:path" — prefix node: menandakan module bawaan Node.js, bukan package di node_modules. Dua baris berikutnya memangkas path menjadi nama berkas dan nama folder dengan path.basename(). Baris terakhir membandingkan __dirname dengan folder kerja proses: keduanya sama saat berkas dijalankan dari foldernya sendiri, tetapi berbeda begitu kita menjalankan node dari folder lain — saat itulah __dirname lebih dapat diandalkan daripada path relatif.
CommonJS telah lama teruji, tetapi JavaScript resmi mengadopsi standar module sendiri dengan sintaks import dan export. Standar modern itulah yang kini menjadi arah utama ekosistem — kita pelajari pada Chapter ES Modules.