CommonJS'te bir dosyanın dışarıya ne vereceğini module.exports belirler; require() çağrısı her zaman bu değeri döndürür. exports ise yalnızca module.exports nesnesine işaret eden kısa bir değişkendir: exports.topla = ... gibi alan eklediğinizde çalışır, ama exports = {...} diye yeni bir değer atadığınızda bağlantı kopar ve modül boş bir nesne dışa aktarır. Node ayrıca her modülü ilk yüklemeden sonra dosya yoluna göre önbelleğe alır; aynı dosyayı ikinci kez require ettiğinizde kod yeniden çalışmaz, aynı nesne geri gelir.

Bu yazıda CommonJS'in iç işleyişini ayrıntılı ele alıyoruz: modül sarmalayıcısı, exports kısayolunun tuzağı, önbellek ve require.cache, require() çözümleme algoritması, node_modules araması ve döngüsel bağımlılıklar. Node'un bir dosyayı CommonJS mi ESM mi sayacağı ayrı bir konu ve CommonJS ve ESM farkı yazısında anlatılıyor. Örnekler, package.json dosyasında "type": "commonjs" olan bir klasörde Node.js v26.13.0 ile çalıştırıldı. Örnekler Windows üzerinde Node.js v26.13.0 ile çalıştırıldı; çıktılardaki uzun yerel klasör yolu okunabilirlik için C:\proje olarak kısaltıldı. Linux ve macOS'ta aynı yollar /home/kullanici/proje/... biçiminde görünür.

Modül sarmalayıcısı: require ve module nereden geliyor?

Modül sarmalayıcısı: require ve module nereden geliyor?

Node.js belgelerine göre her dosya ayrı bir modüldür ve kod çalıştırılmadan önce şu biçimde bir fonksiyonun içine sarılır: (function(exports, require, module, __filename, __dirname) { ... }). Bu sarmalayıcı iki iş görür. Birincisi, dosyanın en üst düzeyinde var, let veya const ile tanımladığınız değişkenler global nesneye değil, o modüle ait kalır; iki dosyada aynı adla değişken tanımlamanız çakışmaya yol açmaz. İkincisi, global gibi görünen ama aslında her modüle özel olan beş değişkeni sağlar.

Bunu kendiniz de görebilirsiniz. Modülün en üst düzeyinde arguments nesnesinin uzunluğu, sarmalayıcıya verilen beş argümanı gösterir. Aynı örnekte exports değişkeninin başlangıçta module.exports ile aynı nesne olduğu, require.main === module karşılaştırmasıyla dosyanın doğrudan node ile çalıştırılıp çalıştırılmadığı ve uzantısız bir require çağrısının JSON dosyasını bulduğu da görülüyor:

javascript
// ex5.js
console.log(arguments.length);
console.log(typeof exports, exports === module.exports);
console.log(require.main === module);
console.log(require('./ayar').port);
console.log(require.resolve.paths('node:fs'));
console.log(require.resolve('./ayar').endsWith('ayar.json'));
console.log(require.resolve.paths('lodash').slice(0, 2));

// çıktı: 5
// çıktı: object true
// çıktı: true
// çıktı: 3000
// çıktı: null
// çıktı: true
// çıktı: [
// çıktı:   'C:\\proje\\req\\node_modules',
// çıktı:   'C:\\proje\\node_modules'
// çıktı: ]

require.main === module kontrolü, bir dosyanın hem kütüphane olarak içe alınabilmesini hem de doğrudan çalıştırıldığında bir şey yapmasını sağlayan bilinen bir kalıptır: node ex5.js ile çalıştırınca true, başka bir dosyadan require('./ex5') ile yükleyince false olur. require.resolve.paths() ise bir paket adı için hangi node_modules klasörlerinin sırayla taranacağını döndürür; belirteç yerleşik bir modülü gösteriyorsa sonuç null olur.

exports ile module.exports arasındaki fark

Belgeler exports değişkenini "module.exports için yazması daha kısa bir referans" olarak tanımlar. Modül çalışmaya başlamadan önce exports değişkenine module.exports nesnesi atanır. require() ise sonunda exports değişkenini değil, module.exports değerini döndürür. Bu yüzden üç kalıp farklı sonuç verir:

javascript
// yanlis.js
exports = { topla: (a, b) => a + b };

// dogru.js
module.exports = { topla: (a, b) => a + b };

// kisa.js
exports.topla = (a, b) => a + b;
exports.carp = (a, b) => a * b;

// sinif.js
module.exports = class Kullanici {
  constructor(ad) { this.ad = ad; }
};

// ex1.js
console.log(require('./yanlis.js'));
console.log(require('./dogru.js'));
console.log(require('./kisa.js'));
const Kullanici = require('./sinif.js');
console.log(new Kullanici('Can'));

// çıktı: {}
// çıktı: { topla: [Function: topla] }
// çıktı: { topla: [Function (anonymous)], carp: [Function (anonymous)] }
// çıktı: Kullanici { ad: 'Can' }

yanlis.js dosyasında exports artık yeni bir nesneyi gösteriyor, ama module.exports hâlâ başlangıçtaki boş nesne. Sonuç {}. Kural basit: alan eklerken exports.ad = ... kısayolunu kullanabilirsiniz; modülün dışa aktardığı değeri bütünüyle değiştirecekseniz (bir fonksiyon, bir sınıf ya da yeni bir nesne) mutlaka module.exports değişkenine atayın. Belgeler bu ikisini aynı anda yeniden bağlamak için module.exports = exports = function Constructor() {} kalıbının da yaygın olduğunu not ediyor.

javascript
// gecikmeli.js
setTimeout(() => { module.exports = { a: 'merhaba' }; }, 0);

// ex8.js
const x = require('./gecikmeli');
console.log(x.a);
setTimeout(() => console.log(require('./gecikmeli').a), 10);

// çıktı: undefined
// çıktı: merhaba

İkinci satır ilginç bir ayrıntıyı gösteriyor: önbellekteki modül kaydı module.exports değerini tuttuğu için, atama yapıldıktan sonra yapılan yeni bir require yeni değeri görüyor. Ama ilk çağrıyı yapan kod elindeki eski referansla kalıyor. Uygulamada bu, modülün kimin ne zaman yüklediğine göre farklı davranması demektir; bu yüzden atamayı geciktirmeyin.

Modül önbelleği ve require.cache

Belgelerin "Caching" bölümüne göre modüller ilk yüklemeden sonra önbelleğe alınır ve aynı dosyaya çözümlenen her require('foo') çağrısı tam olarak aynı nesneyi döndürür. Modülün kodu da yalnızca bir kez çalışır. Bu, yapılandırma ya da veritabanı bağlantısı gibi tek bir örnek (singleton) paylaşmanın doğal yoludur. Bir modülün kodunu birden fazla kez çalıştırmak istiyorsanız belgelerin önerisi bir fonksiyon dışa aktarıp onu çağırmaktır.

Önbellek require.cache nesnesinde, tam dosya yolu anahtarıyla durur. Bir kaydı silerseniz bir sonraki require dosyayı yeniden yükler. Anahtarı elle yazmak yerine require.resolve() ile almak en güvenli yoldur, çünkü bu fonksiyon modülü yüklemeden yalnızca çözümlenmiş dosya adını döndürür:

javascript
// sayac.js
console.log('sayac.js çalıştı');
let n = 0;
module.exports = { artir: () => ++n };

// ex2.js
const a = require('./sayac');
const b = require('./sayac.js');
a.artir();
console.log(a === b, b.artir());
delete require.cache[require.resolve('./sayac')];
const c = require('./sayac');
console.log(a === c, c.artir());

// çıktı: sayac.js çalıştı
// çıktı: true 2
// çıktı: sayac.js çalıştı
// çıktı: false 1

'./sayac' ve './sayac.js' aynı dosyaya çözümlendiği için tek bir kayıt paylaşılıyor ve sayaç ortak. Kayıt silindikten sonra dosya baştan çalışıyor ve yeni bir nesne geliyor; eski a referansı ise eski nesneyi tutmaya devam ediyor. Belgeler, bu yöntemin yerel eklentilerde (native addon) işe yaramadığını, yeniden yüklemenin hata vereceğini belirtiyor. Ayrıca require.cache yerleşik modüllerden önce kontrol edildiği için oraya fs gibi yerleşik bir modül adıyla kayıt eklerseniz require('fs') sizin kaydınızı döndürür; yalnızca node: önekli çağrılar gerçek modülü alır.

Büyük/küçük harf duyarsız dosya sistemlerinde önbellek

Belgelerdeki önemli bir uyarı: önbellek çözümlenmiş dosya adına göre çalıştığı için, büyük/küçük harf ayrımı yapmayan dosya sistemlerinde require('./foo') ve require('./FOO') aynı dosyayı gösterse bile iki farklı modül olarak yüklenir. Windows'ta denediğimizde tam olarak bu oldu:

javascript
// ex3.js
const k1 = require('./sayac');
const k2 = require('./SAYAC');
console.log(k1 === k2);

// çıktı: sayac.js çalıştı
// çıktı: sayac.js çalıştı
// çıktı: false

Dosya iki kez çalıştı ve iki ayrı durum oluştu. Linux gibi büyük/küçük harfe duyarlı bir sistemde ise ./SAYAC hiç bulunamaz ve MODULE_NOT_FOUND alırsınız. Yani bu tür bir yazım hatası Windows'ta sessiz bir çift yüklemeye, sunucuda ise çökmeye dönüşür. İçe aktarma yollarını dosya adlarıyla birebir aynı harflerle yazmak bu sorunu baştan önler.

require() bir modülü nasıl bulur?

require() bir modülü nasıl bulur? Yerleşik modül mü?, Dosya yolu mu?, # ile mi başlıyor?, Paket kendi adıyla mı çağrılıyor?

Belgelerin "All together" bölümü require(X) çağrısının izlediği algoritmayı sözde kodla verir. Özetle sıra şöyledir:

  • Yerleşik modül mü? X bir çekirdek modülse (örneğin fs, http) doğrudan o döner. require('http'), aynı adla bir dosya olsa bile her zaman yerleşik HTTP modülünü verir.
  • Dosya yolu mu? X, ./, ../ ya da / ile başlıyorsa önce dosya olarak (X, sonra X.js, X.json, X.node), sonra klasör olarak denenir; bulunamazsa hata fırlatılır.
  • # ile mi başlıyor? Paketin package.json dosyasındaki "imports" alanına bakılır.
  • Paket kendi adıyla mı çağrılıyor? "exports" alanı olan bir paket içinden kendi adınızla içe aktarma (self-reference) çözülür.
  • node_modules araması: Geçerli modülün klasöründen başlayarak her üst klasörde node_modules/X aranır; paketin "exports" alanı varsa çözümleme ona göre yapılır.
  • Bulunamadıysa MODULE_NOT_FOUND hatası fırlatılır.

Belgelerdeki örnekle, /home/ry/projects/foo.js dosyası require('bar.js') çağırırsa Node sırasıyla /home/ry/projects/node_modules/bar.js, /home/ry/node_modules/bar.js, /home/node_modules/bar.js ve /node_modules/bar.js yollarına bakar. Zaten node_modules ile biten bir yola tekrar node_modules eklenmez. Bu düzen, her projenin bağımlılıklarını kendi klasöründe tutmasını ve farklı projelerin farklı sürümler kullanabilmesini sağlar.

Tüm bu klasörlerde bulunamazsa Node, NODE_PATH ortam değişkenindeki yollara ve tarihsel nedenlerle $HOME/.node_modules, $HOME/.node_libraries ve $PREFIX/lib/node klasörlerine de bakar. NODE_PATH Linux ve macOS'ta iki nokta (:), Windows'ta noktalı virgül (;) ile ayrılır. Belgeler bağımlılıkları yerel node_modules klasöründe tutmayı güçlü biçimde öneriyor; hem daha hızlı hem daha güvenilir yükleniyor.

Klasörü modül olarak yüklemek (Legacy)

require() bir klasör yolu aldığında önce o klasördeki package.json dosyasının "main" alanına, o yoksa index.js ve index.node dosyalarına bakar. Bu davranış belgelerde Stability: 3 - Legacy olarak işaretli ve yerine paket alt yol dışa aktarımları (subpath exports) ya da alt yol içe aktarımları (subpath imports) öneriliyor; çünkü klasör yükleme ESM'de hiç çalışmaz ve import('./kutuphane') çağrısı ERR_UNSUPPORTED_DIR_IMPORT verir.

javascript
// kutuphane/package.json  ->  { "main": "./lib/giris.js" }
// kutuphane/lib/giris.js
module.exports = 'kutuphane/lib/giris.js yüklendi';

// ex7.js
console.log(require('./kutuphane'));
const fs1 = require('fs');
const fs2 = require('node:fs');
console.log(fs1 === fs2);
console.log(module.paths.length > 0, typeof module.id);

// çıktı: kutuphane/lib/giris.js yüklendi
// çıktı: true
// çıktı: true string

Yerleşik modülleri node: önekiyle yüklemek önbelleği atlar ve her zaman gerçek modülü verir. Belgelere göre node:sqlite, node:test, node:test/reporters, node:sea ve node:ffi gibi bazı yeni modüller yalnızca önekle yüklenebilir; bu kural aynı adı daha önce almış npm paketleriyle çakışmayı önlemek için konmuş.

Döngüsel bağımlılıklar

İki modül birbirini require ettiğinde sonsuz döngüye girilmez. Belgelerin anlattığı mekanizma şu: a.js b.js dosyasını yüklerken b.js de a.js dosyasını isterse, b.js o anda a.js modülünün henüz bitmemiş bir kopyasını alır. Yani a.js o ana kadar exports nesnesine ne eklediyse yalnızca onu görür. Örneği Türkçe değişkenlerle çalıştırdık:

javascript
// a.js
console.log('a başladı');
exports.hazir = false;
const b = require('./b.js');
console.log('a içinde b.hazir =', b.hazir);
exports.hazir = true;
console.log('a bitti');

// b.js
console.log('b başladı');
exports.hazir = false;
const a = require('./a.js');
console.log('b içinde a.hazir =', a.hazir);
exports.hazir = true;
console.log('b bitti');

// ex4.js
const a = require('./a.js');
const b = require('./b.js');
console.log('main: a.hazir =', a.hazir, 'b.hazir =', b.hazir);

// çıktı: a başladı
// çıktı: b başladı
// çıktı: b içinde a.hazir = false
// çıktı: b bitti
// çıktı: a içinde b.hazir = true
// çıktı: a bitti
// çıktı: main: a.hazir = true b.hazir = true

b.js içinde a.hazir değerinin false görünmesi, eksik nesnenin tipik belirtisidir. Bu yüzden döngüdeki modüllerde const { fonksiyon } = require('./a') gibi yıkım (destructuring) yapmak tehlikelidir: o anda alan henüz tanımlanmamışsa değişken undefined olarak kalır ve sonradan dolmaz. Nesnenin kendisini tutup alanlara kullanım anında erişmek ya da ortak kodu üçüncü bir modüle çıkarmak döngüyü zararsız hale getirir. Belgeler kısaca "dikkatli planlama gerekir" diyor.

MODULE_NOT_FOUND hatasını okumak

Bir modül bulunamadığında CommonJS yükleyicisi code alanı MODULE_NOT_FOUND olan bir hata fırlatır. Belgelere göre v12.0.0'dan beri bu hataya bir requireStack alanı da ekleniyor; hangi dosyanın hangi dosyayı isterken hata aldığını gösteren bir dizi:

javascript
// ex6.js
try {
  require('./yok-boyle-dosya');
} catch (err) {
  console.log(err.code);
  console.log(err.message.split('\n')[0]);
  console.log(err.requireStack.length);
}

// çıktı: MODULE_NOT_FOUND
// çıktı: Cannot find module './yok-boyle-dosya'
// çıktı: 1

ESM tarafındaki karşılığı ERR_MODULE_NOT_FOUND koddur; ikisi farklı yükleyicilerden gelir. Bu kodların ve ERR_REQUIRE_ASYNC_MODULE gibi diğer modül hatalarının listesi için Node.js ERR_* hata kodları yazısına bakabilirsiniz.

module nesnesinin işe yarayan alanları

module nesnesinin işe yarayan alanları: module.exports, module.id, module.filename, module.path
AlanNe verir
module.exportsModülün dışa aktardığı değer; require() bunu döndürür
module.idModülün kimliği; genellikle tam dosya yolu, ana modülde "."
module.filenameTam çözümlenmiş dosya adı
module.pathModülün klasörü
module.pathsPaketler için taranacak node_modules yolları
module.loadedModülün yüklenmesi bitti mi
module.childrenBu modülün ilk kez yüklediği modüller
module.parentDeprecated; yerine require.main ve module.children
require.resolve(x)Yüklemeden çözümlenmiş dosya adı
require.extensionsDeprecated (v0.10.6); kullanmayın

Sık yapılan hatalar

  • exports = ... yazmak: Modül boş nesne döndürür. Değerin tamamını değiştirmek için module.exports kullanın.
  • module.exports atamasını geciktirmek: setTimeout ya da bir Promise içinde yapılan atamayı ilk tüketici görmez.
  • Önbelleği unutmak: Bir modülün her require çağrısında yeniden çalışacağını sanmak. Kod bir kez çalışır; tekrar çalıştırmak için fonksiyon dışa aktarın.
  • İçe aktarma yolunda harf farkı: Windows'ta çift yükleme, Linux sunucuda MODULE_NOT_FOUND olarak geri döner.
  • Döngüsel modülde yıkım yapmak: const { x } = require('./a') eksik nesneden undefined alabilir.
  • .cjs uzantısını atlamak: require() yalnızca .js, .json ve .node uzantılarını tahmin eder; require('./dosya') yazarak dosya.cjs yüklenmez, tam adı yazmalısınız.

Sık Sorulan Sorular

require() senkron mu çalışır?

Evet. require() dosyayı bulur, okur ve çalıştırır, sonra module.exports değerini döndürür; bu sırada kodunuz bekler. Bu yüzden genellikle dosyanın en üstünde, uygulama başlarken çağrılır. Asenkron yükleme gerekiyorsa import() ifadesini CommonJS dosyalarında da kullanabilirsiniz.

Bir JSON dosyasını require ile okumak doğru mu?

require('./ayar.json') dosyayı JSON olarak ayrıştırıp bir nesne döndürür ve sonucu önbelleğe alır. Dosya çalışma sırasında değişiyorsa ikinci require yeni içeriği görmez. Değişebilen dosyalar için fs.readFile ile okuyup JSON.parse kullanmak daha doğrudur.

require.cache silmek "hot reload" için güvenli mi?

Kaydı silmek yalnızca bir sonraki require çağrısını etkiler; daha önce alınmış referanslar eski nesneyi tutar ve eski modülün açtığı zamanlayıcılar ya da bağlantılar kapanmaz. Belgeler require.cache ile oynarken dikkatli olunmasını söylüyor. Geliştirme sırasında dosya değişince yeniden başlatma için node --watch kullanımı daha temiz bir yoldur.

NODE_PATH kullanmalı mıyım?

Belgelere göre NODE_PATH hâlâ destekleniyor ama ekosistem bağımlılıkları yerel node_modules klasöründe tutmaya yerleştiği için artık pek gerekli değil ve ayarlandığını bilmeyen ekiplerde şaşırtıcı davranışlara yol açabiliyor. Proje içi kısa yollar için package.json "imports" alanı daha iyi bir seçenek.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: Node.js öğrenme rehberi · CommonJS ve ESM farkı · package.json exports ve imports · __dirname ve import.meta · Node.js hata kodları · JavaScript closure · Node.js nedir?