Node.js iki modül sistemini aynı anda destekler: Node'un ilk günden beri kullandığı CommonJS (require() ve module.exports) ve JavaScript standardının parçası olan ECMAScript modülleri, kısaca ESM (import ve export). Bir dosyanın hangi sistemle çalışacağına dosya uzantısı (.mjs ya da .cjs), en yakın package.json dosyasındaki "type" alanı veya --input-type bayrağı karar verir. Bu işaretlerin hiçbiri yoksa Node kaynak koda bakar ve ESM sözdizimi bulursa dosyayı ESM olarak çalıştırır.
Bu yazı dilin import/export sözdizimini değil, Node.js çalışma ortamının iki sistemi nasıl ayırt ettiğini, birbirine nasıl bağladığını ve geçişte en sık hangi hata kodlarının çıktığını anlatıyor. Sözdiziminin kendisi için JavaScript ES modülleri yazısına, CommonJS'in iç işleyişi için require ve module.exports rehberine bakabilirsiniz. Tüm örnekler Node.js v26.13.0 ile çalıştırıldı ve yorumlardaki çıktılar gerçek çıktılardır.
Node bir dosyanın modül türüne nasıl karar verir?

Node.js belgeleri bu kararı açık işaretler ve belirsiz girdi olarak ikiye ayırır. Açık işaretler şunlardır: .mjs uzantısı her zaman ESM, .cjs uzantısı her zaman CommonJS demektir ve en yakın package.json içinde ne yazdığı bu iki uzantıyı etkilemez. .js uzantılı dosyalarda ise en yakın package.json dosyasının en üst düzeydeki "type" alanına bakılır: "module" ise ESM, "commonjs" ise CommonJS. Komut satırından --eval ile verilen ya da standart girdiden gelen kod için --input-type=module veya --input-type=commonjs aynı görevi görür.
"En yakın package.json" ifadesi önemlidir. Node, dosyanın bulunduğu klasörden başlayıp üst klasörlere doğru ilk package.json dosyasını arar; bir node_modules klasörüne ya da diskin köküne gelince durur. Yani projenizin kökündeki "type": "module" ayarı alt klasörlerdeki tüm .js dosyalarını etkiler, ama node_modules içindeki bir paket kendi package.json dosyasına göre yorumlanır. Bağımlılıklarınızın sizin ayarınızdan etkilenmemesi bu sayede sağlanır.
| Dosya / girdi | Koşul | Sonuç |
|---|---|---|
| app.mjs | Her zaman | ESM |
| app.cjs | Her zaman | CommonJS |
| app.js | En yakın package.json içinde "type": "module" | ESM |
| app.js | En yakın package.json içinde "type": "commonjs" | CommonJS |
| app.js veya uzantısız | type alanı yok ya da package.json yok | Önce CommonJS denenir, ESM sözdizimi bulunursa ESM |
| --eval veya STDIN | --input-type=module / commonjs | Bayrağa göre; bayrak yoksa sözdizimi tespiti |
Sözdizimi tespiti (syntax detection) ve uyarı mesajı
Uzantısı .js olan ya da hiç uzantısı olmayan bir dosya için ne package.json ne de "type" alanı bir şey söylemiyorsa, belgeler buna belirsiz girdi der. Node bu durumda kodu önce CommonJS olarak ayrıştırmaya çalışır; ayrıştırıcı CommonJS'te geçersiz olan bir ESM yapısıyla karşılaşırsa kodu bu kez ESM olarak yeniden değerlendirir. ESM sayılan yapılar şunlardır: import deyimleri (dinamik import() ifadesi değil, çünkü o CommonJS'te de geçerlidir), export deyimleri, import.meta kullanımı, modülün en üst düzeyinde await ve CommonJS sarmalayıcı değişkenlerinin (require, module, exports, __dirname, __filename) const/let ile yeniden tanımlanması.
Sözdizimi tespiti belgelerde Stability: 1.2 - Release candidate olarak işaretli ve v22.7.0 ile v20.19.0 sürümlerinden beri varsayılan olarak açık. Belgeler bu yeniden ayrıştırmanın bir performans maliyeti olduğunu açıkça yazıyor. Aşağıdaki örnekte dosyanın bulunduğu klasörde package.json yok, ama üst klasörlerden birinde "type" alanı olmayan bir package.json var. Node kodu çalıştırıyor ama bir uyarı basıyor:
// tespit/app.js (klasörde package.json yok, üst klasördekinde "type" alanı yok)
import { sep } from 'node:path';
console.log('ESM olarak çalıştı, sep uzunluğu:', sep.length);
// çıktı: (node:87132) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///C:/proje/tespit/app.js is not specified and it doesn't parse as CommonJS.
// çıktı: Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
// çıktı: To eliminate this warning, add "type": "module" to \\?\C:\Users\kullanici\package.json.
// çıktı: (Use `node --trace-warnings ...` to show where the warning was created)
// çıktı: ESM olarak çalıştı, sep uzunluğu: 1Uyarının kendisi çözümü de söylüyor: ilgili package.json dosyasına "type": "module" eklemek ya da dosyayı .mjs olarak adlandırmak. Node.js Learn'deki paket yayınlama rehberi de sözdizimi tespitinin doğru paket yapılandırmasının yerini tutmadığını, her durumu yakalayamadığını ve ciddi bir performans maliyeti olduğunu vurguluyor. Bu uyarıyı görüyorsanız onu susturmaya değil, işareti açıkça koymaya çalışın.
ESM dosyasından CommonJS modülü yüklemek

ESM tarafında import deyimi hem ESM hem de CommonJS modüllerini yükleyebilir. Bir CommonJS modülü import edildiğinde Node onun etrafında bir isim alanı (namespace) nesnesi kurar. Bu nesnenin default anahtarı her zaman CommonJS tarafındaki module.exports değerini gösterir; v23.0.0'dan beri aynı değer 'module.exports' adıyla da sunulur. Ayrıca Node, CommonJS dosyasının kaynak metnini statik olarak analiz edip exports.selam = ... gibi kalıplardan isimli dışa aktarımları çıkarmaya çalışır. Böylece çoğu durumda import { selam } from './eski.cjs' yazabilirsiniz.
// esm/package.json -> { "type": "module" }
// esm/eski.cjs
exports.selam = (ad) => `Merhaba ${ad}`;
exports.surum = 3;
// esm/hesap.js
export const kdv = (tutar) => tutar * 1.2;
export default 'hesap modülü';
// esm/app.js
import { kdv } from './hesap.js';
import eski, { selam } from './eski.cjs';
import * as ns from './eski.cjs';
console.log(kdv(100));
console.log(selam('Ayşe'));
console.log(eski);
console.log(ns);
console.log(typeof require, typeof module);
// çıktı: 120
// çıktı: Merhaba Ayşe
// çıktı: { selam: [Function (anonymous)], surum: 3 }
// çıktı: [Module: null prototype] {
// çıktı: default: { selam: [Function (anonymous)], surum: 3 },
// çıktı: 'module.exports': { selam: [Function (anonymous)], surum: 3 },
// çıktı: selam: [Function (anonymous)],
// çıktı: surum: 3
// çıktı: }
// çıktı: undefined undefinedBurada iki ayrıntıya dikkat edin. Birincisi, statik analizle çıkarılan isimli dışa aktarımlar modül yüklendiği andaki değerlerin kopyasıdır: CommonJS modülü daha sonra module.exports nesnesine yeni bir alan eklerse ya da mevcut bir alanı değiştirirse, bu değişiklik isimli import'a yansımaz. İkincisi, analiz yaygın kalıpları tanır ama her yazım biçimini yakalayamaz. Belgeler, isimli import beklediğiniz gibi gelmediğinde import paket from '...' biçimindeki varsayılan import'u kullanmanızı öneriyor; o yol her zaman module.exports değerinin kendisini verir.
CommonJS dosyasından ESM yüklemek: require(esm)
Uzun süre CommonJS tarafından ESM yüklemenin tek yolu asenkron import() ifadesiydi ve require() bir ESM dosyasına ERR_REQUIRE_ESM hatası veriyordu. Bu durum değişti: belgelerdeki YAML kaydına göre require() ile ESM yükleme v22.0.0 ve v20.17.0 ile geldi, v23.0.0, v22.12.0 ve v20.19.0 sürümlerinde bayrak gerektirmez oldu ve v25.4.0 ile deneysel olmaktan çıktı. ERR_REQUIRE_ESM hata kodu artık belgelerde Deprecated olarak işaretli.
Koşul şu: yüklenen ESM modülü ve onun import ettiği modüllerin tamamı senkron olmalı, yani hiçbirinde en üst düzeyde await bulunmamalı. Koşul sağlanırsa require() modülün isim alanı nesnesini doğrudan döndürür. Varsayılan dışa aktarım bu nesnenin default alanındadır; modül bir varsayılan dışa aktarım içeriyorsa Node, derleme araçlarıyla uyum için __esModule: true alanını da ekler. Belgeler bu alanın deneysel olduğunu ve elle yazılan CommonJS kodunun ona güvenmemesi gerektiğini belirtiyor. Modül grafiğinde top-level await varsa ERR_REQUIRE_ASYNC_MODULE fırlatılır; o zaman import() kullanmanız gerekir.
// cjs/package.json -> { "type": "commonjs" }
// cjs/hesap.mjs
export const kdv = (tutar) => tutar * 1.2;
export default class Sepet {}
// cjs/bekle.mjs
export const ayar = await Promise.resolve({ port: 3000 });
// cjs/app.js
const hesap = require('./hesap.mjs');
console.log(hesap);
console.log(hesap.kdv(50));
console.log(process.features.require_module);
try {
require('./bekle.mjs');
} catch (err) {
console.log(err.code);
}
import('./bekle.mjs').then((m) => console.log(m.ayar));
// çıktı: [Module: null prototype] {
// çıktı: __esModule: true,
// çıktı: default: [class Sepet],
// çıktı: kdv: [Function: kdv]
// çıktı: }
// çıktı: 60
// çıktı: true
// çıktı: ERR_REQUIRE_ASYNC_MODULE
// çıktı: { port: 3000 }Hatanın tam mesajı sorunun kaynağını da gösterir: "require() cannot be used on an ESM graph with top-level await. Use import() instead." Mesajın devamında hangi modülün istendiği ve Require stack listesi yer alır. Top-level await ifadesinin grafiğin neresinde olduğunu bulmak için belgeler --experimental-print-required-tla bayrağını gösteriyor. Özelliğin çalışma zamanında açık olup olmadığını process.features.require_module ile kontrol edebilir, beklenmedik bir kırılma yaşarsanız --no-require-module ile kapatabilir, nerelerde kullanıldığını görmek için --trace-require-module bayrağını kullanabilirsiniz.
Çözümleme farkları: uzantı ve klasör kuralları
İki sistem yalnızca sözdiziminde değil, bir belirtecin (specifier) hangi dosyaya karşılık geldiğini bulma biçiminde de ayrışır. require() uzantı tahmin eder: require('./yardimci') sırasıyla .js, .json ve .node uzantılarını dener, sonra klasörü modül gibi yüklemeye (package.json içindeki main ya da index.js) çalışır. ESM yükleyicisi ise tarayıcılardaki davranışı izler: göreli ve mutlak belirteçlerde dosya uzantısı zorunludur, klasör index dosyası (örneğin ./startup/index.js) tam yazılmalıdır ve belirteçler URL olarak çözümlenir. URL olarak çözümlendiği için # ve ? gibi özel karakterlerin yüzde kodlanması gerekir.
// uzanti/package.json -> { "type": "module" }
// uzanti/yardimci.js
export const x = 1;
// uzanti/app.js
try {
await import('./yardimci');
} catch (err) {
console.log(err.code);
console.log(err.message.split('\n').at(-1));
}
try {
await import('./');
} catch (err) {
console.log(err.code);
}
const m = await import('./yardimci.js');
console.log(m.x);
// çıktı: ERR_MODULE_NOT_FOUND
// çıktı: Did you mean to import "./yardimci.js"?
// çıktı: ERR_UNSUPPORTED_DIR_IMPORT
// çıktı: 1ERR_MODULE_NOT_FOUND mesajının ilk satırı tam dosya yolunu ve modülü kimin import ettiğini gösterir; son satırdaki "Did you mean" önerisi çoğu zaman eksik uzantıyı doğrudan söyler. Klasör import etmeye çalıştığınızda ise ERR_UNSUPPORTED_DIR_IMPORT alırsınız. Bu hata kodlarının tamamı ve çözümleri için ERR_MODULE_NOT_FOUND ve diğer hata kodları yazısına bakabilirsiniz.
ESM'de olmayan CommonJS değişkenleri ve karşılıkları

CommonJS modülleri bir fonksiyon sarmalayıcısının içinde çalıştığı için require, exports, module, __filename ve __dirname her dosyada hazır gelir. ESM'de bu sarmalayıcı yoktur; yukarıdaki örnekte typeof require ifadesinin undefined döndürmesi bundandır. Belgeler her biri için bir karşılık veriyor:
| CommonJS | ESM karşılığı |
|---|---|
| require() | import / import(); gerekirse module.createRequire(import.meta.url) |
| __filename, __dirname | import.meta.filename, import.meta.dirname |
| require.main === module | import.meta.main (Stability: 1.0 Early development) |
| require.resolve() | import.meta.resolve() veya new URL('./x', import.meta.url) |
| require.cache | Yok; ESM yükleyicisinin kendi ayrı önbelleği var |
| require.extensions | Yok; yerine modül özelleştirme kancaları (customization hooks) |
| NODE_PATH | import çözümlemesinde kullanılmaz; belgeler sembolik bağlantı öneriyor |
| require('./x.json') | import x from './x.json' with { type: 'json' } |
Bir ESM dosyasında gerçekten require gerekiyorsa, örneğin yerel bir eklenti (addon) yüklemek ya da bir JSON dosyasını senkron okumak için, node:module modülündeki createRequire() fonksiyonu bir require üretir. JSON dosyasını import etmek isterseniz with { type: 'json' } import özniteliği zorunludur. Belgelere göre JSON modülü için CommonJS önbelleğinde de bir kayıt oluşturulur; aynı dosyayı iki yoldan yüklediğinizde aynı nesneyi alırsınız:
// createreq/app.js (package.json: { "type": "module" })
import { createRequire } from 'node:module';
import ayar2 from './ayar.json' with { type: 'json' };
const require = createRequire(import.meta.url);
const ayar = require('./ayar.json');
console.log(ayar.port, ayar2.port, ayar === ayar2);
// çıktı: 8080 8080 trueimport.meta özelliklerinin ayrıntısı ve dosya yolu üretme örnekleri __dirname ve import.meta yazısında yer alıyor.
CommonJS mi ESM mi: hangisini seçmeli?
Belgelerde "şunu kullanın" diye kesin bir hüküm yok; ama yönlendirme açık. ESM, belgelerin ifadesiyle JavaScript kodunu yeniden kullanmak için paketlemenin resmî standart biçimi ve tarayıcılar ile diğer çalışma ortamlarında da aynı şekilde çalışıyor. Yeni bir uygulamaya başlıyorsanız kökteki package.json dosyasına "type": "module" yazmak, uzantı ve klasör kurallarını baştan ESM'e göre kurmanızı sağlar. Mevcut ve çalışan bir CommonJS projesini ise acele dönüştürmeniz gerekmez: require(esm) sayesinde artık senkron ESM bağımlılıklarını da doğrudan yükleyebilirsiniz.
Paket yazarları için Node.js Learn'deki rehber daha net konuşuyor: genellikle en iyisi tek bir biçimde, ya CommonJS ya da ESM olarak yayınlamaktır, ikisini birden değil. İki biçimi birlikte dağıtmak dual package hazard denen soruna yol açabilir: uygulama paketi import ile, bağımlılıklardan biri aynı paketi require ile yüklerse aynı paketin iki ayrı kopyası belleğe girer. Paket bir sınıf dışa aktarıyorsa iki kopyadan üretilen nesneler arasında instanceof karşılaştırması false döner; paket durum (state) tutuyorsa iki paralel durum oluşur. Paket yapılandırmasının ayrıntıları package.json exports ve imports yazısında anlatılıyor.
| Konu | CommonJS | ESM |
|---|---|---|
| Yükleme | require(), senkron | import (statik), import() (asenkron) |
| Dışa aktarma | module.exports, exports | export, export default |
| Uzantı tahmini | Var (.js, .json, .node) | Yok, uzantı zorunlu |
| Klasörü modül olarak yükleme | Var (main, index.js), Legacy | Yok (ERR_UNSUPPORTED_DIR_IMPORT) |
| Top-level await | Yok | Var |
| Önbellek | require.cache, dosya yoluna göre | Ayrı önbellek, URL'ye göre |
| Karşı tarafı yükleme | require(esm) yalnızca senkron ESM için | import ile CommonJS her zaman |
Sık yapılan hatalar
"type"alanını yazmamak: Kod çalışsa bile Node her açılışta önce CommonJS deneyip sonra yeniden ayrıştırır veMODULE_TYPELESS_PACKAGE_JSONuyarısı basar. Alanı açıkça yazın.- ESM'de uzantıyı atlamak:
import './db'CommonJS alışkanlığıdır; ESM'de./db.jsyazmanız gerekir, yoksaERR_MODULE_NOT_FOUNDalırsınız. - Top-level await kullanan modülü require etmek:
ERR_REQUIRE_ASYNC_MODULEhatası alırsınız. Ya bekleyen kodu bir fonksiyonun içine taşıyın ya da tüketici taraftaimport()kullanın. - CommonJS isimli import'una canlı değer gibi güvenmek: Statik analizle çıkarılan isimler yükleme anındaki kopyadır; sonradan değişen değerler için varsayılan import ile
module.exportsnesnesine erişin. - ESM dosyasında
__dirnamekullanmak:ReferenceErrorverir;import.meta.dirnamekullanın. - Eski kılavuzlardaki
ERR_REQUIRE_ESMçözümlerini uygulamak: Bu kod artık deprecated; güncel Node sürümlerinde senkron ESM modülleri doğrudanrequireedilebiliyor.
Sık Sorulan Sorular
Aynı projede .mjs ve .cjs dosyalarını birlikte kullanabilir miyim?
Evet. Belgeler bunu özellikle anlatır: "type": "module" olan bir pakette tek bir dosyayı CommonJS yapmak için ona .cjs, "type": "commonjs" olan bir pakette tek bir dosyayı ESM yapmak için .mjs uzantısı verebilirsiniz. Uzantı her zaman "type" alanından önce gelir.
require() ile ESM yüklemek artık güvenli mi?
Belgelere göre özellik v25.4.0'dan beri deneysel değil ve v26 belgelerinde koşulları net: modül grafiği senkron olmalı. Dönen değer bir isim alanı nesnesidir; varsayılan dışa aktarımı .default üzerinden okursunuz. Top-level await içeren modüllerde hâlâ import() gerekir.
--input-type bayrağı ne işe yarar?
Dosya olmadan çalıştırılan kod, yani --eval ile verilen ya da standart girdiden okunan metin için modül türünü belirler. --input-type=module metni ESM, --input-type=commonjs CommonJS olarak çalıştırır. Bayrak verilmezse metin belirsiz girdi sayılır ve sözdizimi tespiti devreye girer.
TypeScript dosyalarında bu kurallar nasıl işliyor?
Node, .ts, .mts ve .cts uzantılarını TypeScript metni olarak tanır; modül türü kuralları .mts/.cts uzantıları ve "type" alanı üzerinden benzer şekilde uygulanır. Ayrıntılar için TypeScript ile Node.js yazısına bakın.
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 · require ve module.exports · package.json exports ve imports · __dirname ve import.meta · Node.js hata kodları · JavaScript ES modülleri · Node.js nedir?