Di balik layar setiap aplikasi Node.js, ada sebuah “panggung” tempat modul-modul bekerja sama: saling memanggil, berbagi fungsi, dan mengatur alur logika. Namun, panggung itu ternyata tidak hanya satu. Sejak lama, Node.js bertumpu pada CommonJS-format require() yang begitu akrab bagi banyak pengembang. Lalu hadir ECMAScript Modules (ESM) dengan sintaks import dan export, membawa janji standar yang selaras dengan ekosistem JavaScript di browser.
Pertemuan dua dunia ini tidak selalu mulus. Di satu sisi, CommonJS menawarkan kematangan dan kompatibilitas luas dengan paket-paket lama. Di sisi lain, ESM menjanjikan masa depan yang lebih rapi, konsisten, dan teroptimasi. Banyak proyek akhirnya berada di persimpangan: tetap di zona nyaman CommonJS, migrasi penuh ke ESM, atau hidup di tengah-tengah keduanya.
Artikel ini akan membedah modul Node.js dari dua poros utama tersebut: CommonJS dan ESM. Bukan sekadar membandingkan sintaks, tetapi menelusuri bagaimana keduanya bekerja, di mana letak kelebihan dan keterbatasannya, serta apa dampaknya bagi struktur proyek dan proses build. Dengan begitu, pilihan antara CommonJS dan ESM tidak lagi sekadar mengikuti tren, melainkan keputusan teknis yang sadar dan terukur.
Menyingkap Anatomi CommonJS: Cara Kerja require di Balik Layar
Di Node.js, setiap berkas .js yang memakai require sebenarnya dibungkus secara otomatis ke dalam sebuah fungsi khusus. Kira-kira bentuknya seperti ini:
“`js
(function (exports, require, module, __filename, __dirname) {
// Isi file kamu di sini
});
“`
Fungsi pembungkus ini yang membuat variabel seperti module dan exports “muncul begitu saja” tanpa harus kamu definisikan. Saat kamu memanggil require('./math'), Node akan:
- Menemukan path file yang sesuai (misalnya
math.js). - Membaca isi file dari disk.
- Membungkusnya dengan fungsi internal seperti di atas.
- Menjalankan fungsi itu satu kali, lalu menyimpan hasilnya di module cache.
Karena ada cache, setiap pemanggilan require ke modul yang sama tidak akan dieksekusi ulang, cukup mengambil exports yang sudah ada. Ini bikin eksekusi lebih cepat, tapi kadang bisa bikin bingung saat kamu mengubah state di dalam modul. Perilaku ini bisa dirangkum seperti berikut:
| Aspek | Perilaku |
|---|---|
| Pemanggilan | Sinkron, langsung mengembalikan nilai module.exports |
| Eksekusi | Hanya sekali per path, sisanya pakai cache |
| Scope | Terisolasi via fungsi pembungkus internal |
| Ekspor | Menggunakan module.exports atau exports |
Mengurai ESM di Node.js: from Import Sintaks hingga Resolusi Dependency
Begitu mulai bermain dengan ESM di Node.js, hal pertama yang bikin kening berkerut biasanya adalah sintaks import dan export. Berbeda dengan CommonJS yang serba fleksibel lewat require, ESM terasa lebih “kaku” tapi justru rapi dan terprediksi. Kamu akan sering berhadapan dengan pola seperti ini:
“`js
// file: lib/math.js
export const PI = 3.14;
export function tambah(a, b) {
return a + b;
}
export default function kali(a, b) {
return a * b;
}
“`
“`js
// file: index.mjs
import kali, { PI, tambah } from ‘./lib/math.js’;
console.log(PI);
console.log(tambah(2, 3));
console.log(kali(4, 5));
“`
Di balik sintaks yang tampak sederhana itu, ada mesin resolusi dependency yang cukup cerewet. Node.js sekarang lebih tegas soal ekstensi file, tipe modul, dan cara menginterpretasi path. Hal-hal yang sering jadi sumber “error tengah malam” di antaranya:
- Ekstensi wajib jelas:
./lib/math.jsbukan lagi./lib/mathbegitu kamu main di ESM. typedipackage.json:"type": "module"mengubah cara Node membaca semua berkas.jsdi project kamu.- Import dari package: Node akan lihat
exportsdanmodule/maindipackage.jsonpaket tersebut, lalu memilih entry yang cocok dengan ESM.
| Kasus | Perilaku ESM |
|---|---|
| Tanpa ekstensi | Umumnya gagal, Node mau path eksplisit |
| Mix CJS & ESM | Butuh jembatan: createRequire atau dynamic import() |
| Alias path custom | Perlu dukungan bundler / loader tambahan |
Perangkap Umum Migrasi dari CommonJS ke ESM dan Strategi Menghindarinya
Banyak developer kaget waktu sadar bahwa hal-hal yang dulu “aman-aman aja” di CommonJS, tiba-tiba jadi sumber error di ESM. Salah satu jebakan klasik adalah perbedaan cara ekspor-impor. Di CommonJS, kita santai saja pakai module.exports dan require, sedangkan di ESM semuanya serba export dan import. Kalau kita campur tanpa strategi, hasilnya bisa:
- Objek impor kosong atau undefined tanpa pesan error yang jelas
- Loop dependensi yang dulu jalan, tiba-tiba bikin runtime error
- Tools seperti bundler atau test runner jadi bingung membaca struktur modul
| Masalah | CJS | ESM |
|---|---|---|
| Default export | module.exports = fn |
export default fn |
| Import | const lib = require('lib') |
import lib from 'lib' |
Strateginya? Jangan migrasi brutal dalam sekali tarik napas. Mulai dari modul yang paling “sendiri” dulu, baru yang punya banyak relasi. Pastikan juga:
- Selalu konsisten: hindari campur CJS dan ESM dalam satu file
- Gunakan extension eksplisit seperti
.js,.mjs, atau.cjsuntuk menghindari resolusi yang membingungkan - Manfaatkan layer adapter kecil, misalnya satu file ESM yang mengimpor modul CommonJS, lalu mengekspornya ulang dengan gaya ESM
- Cek ulang konfigurasi bundler, test runner, dan
tsconfig(kalau pakai TypeScript), karena mereka sering punya opsi khusus untuk mode modul
Kalau perlu, buat file “jembatan” sementara, misalnya:
// cjs-wrapper.cjs (CommonJS)
const lib = require('./lib-esm.mjs')
module.exports = lib
// lib-esm.mjs (ESM)
export function doSomething() {
// ...
}
Rekomendasi Praktis Memilih Antara CommonJS dan ESM untuk Proyek Node.js Anda
Alih-alih terpaku pada “mana yang lebih modern”, lebih masuk akal untuk melihat konteks proyek Anda. Kalau Anda sedang mengerjakan kode lama yang penuh dengan require dan module.exports, memaksa migrasi total ke ESM hanya demi ikut tren kadang lebih merepotkan daripada manfaatnya. Dalam kasus seperti ini, tetaplah di CommonJS sambil perlahan menulis modul baru dengan ESM, terutama jika Anda sudah mulai mengandalkan bundler modern atau ingin berbagi kode dengan frontend. Untuk proyek baru yang butuh tree-shaking dan integrasi dekat dengan ekosistem front-end, memilih ESM sejak awal biasanya jauh lebih mulus.
- Proyek lama, banyak dependensi jadul: prioritaskan CommonJS.
- Monorepo, berbagi kode client-server: ESM lebih enak dirawat.
- Butuh jalan tengah: gunakan kombinasi dengan sedikit lapisan kompatibilitas.
- Tim beragam: pilih standar yang paling mudah dipahami semua anggota.
| Situasi | Pilihan Tepat | Catatan Singkat |
|---|---|---|
| API internal kecil | CommonJS | Lebih cepat disusun, minim konfigurasi. |
| SSR + frontend modern | ESM | Mudah berbagi utilitas dan tipe. |
| Library open-source | Dual: CJS + ESM | Ramah untuk berbagai environment. |
Pada akhirnya, jangan ragu untuk bersikap pragmatis. Anda bisa memulai dengan satu format, lalu menambahkan yang lain ketika kebutuhan muncul. Contohnya, library yang awalnya hanya CommonJS bisa diekspor ulang ke ESM tanpa mengubah struktur internal secara ekstrem.
“`js
// package.json
{
“name”: “contoh-lib”,
“type”: “module”,
“main”: “dist/index.cjs”,
“module”: “dist/index.mjs”,
“exports”: {
“require”: “./dist/index.cjs”,
“import”: “./dist/index.mjs”
}
}
“`
The Conclusion
Pada akhirnya, perdebatan antara CommonJS dan ESM di Node.js bukan soal siapa yang “paling benar”, melainkan soal memahami konteks, kebutuhan, dan arah ekosistem yang terus bergerak. CommonJS memberi fondasi kokoh yang telah menghidupi jutaan proyek; ESM menawarkan bahasa yang lebih selaras dengan standar JavaScript modern dan dunia browser yang kian menyatu dengan server.
Sebagai pengembang, tugas kita bukan sekadar memilih salah satu, tetapi mengerti cara keduanya bekerja, di mana batasnya, dan kapan masing-masing paling masuk akal digunakan. Di tengah transisi ini, Node.js justru memberi ruang untuk bereksperimen, berevolusi, dan merapikan kembali cara kita menulis modularisasi kode.
Ketika Anda menulis require atau import setelah ini, mungkin bukan lagi sekadar kebiasaan jari di keyboard-melainkan keputusan sadar, hasil dari memahami lapisan-lapisan yang sudah kita bedah bersama. Dunia modul di Node.js belum selesai berubah, dan di situlah justru letak peluang: kita ikut menulis bab berikutnya.

