Module Federation’u bir süre önce Micro Frontend Mimarisi üzerine yazdığım yazıda teorik temeli ele almıştım. Bu yazıda ise doğrudan Module Federation’a odaklanacağız; minimal bir host + remote örneği üzerinden işin pratiğine ineceğiz
🤔 Neden Module Federation?
Micro frontend mimarisine geçmeye karar verdiğinizde ilk karşılaştığınız soru şu: “Bu bağımsız parçaları tarayıcıda nasıl bir araya getireceğim?”
Module Federation öncesinde bu sorunun iki popüler çözümü vardı. İlki, her micro app (Module federation’da remote olarak geçiyor) ’i npm paketi olarak yayınlayıp shell (Module Federation da host olarak geçiyor) uygulamanın build’ine dahil etmekti. Bu yaklaşımın açık bir eksikliği vardı, herhangi bir micro app güncellendiğinde shell uygulamayı yeniden build edip deploy etmek zorundaydık. Yani “bağımsız deployment” vaadi baştan çöpe gidiyordu.
İkinci yaklaşım manuel olarak CDN’den script yüklemekti. Bu gerçekten bağımsızlık sağlıyordu ama ortak bağımlılık problemi diye bilinen meseleyi çözmüyordu. Her micro app kendi React kopyasını ~42kb olarak tarayıcıya indiriyordu. Üç micro app’iniz varsa kullanıcı üç kopya React indiriyordu ve daha da kötüsü, bu kopyalar aynı anda çalıştığında “invalid hook call” gibi garip hatalar alıyorduk.
İşte tam bu noktada Webpack 5 ile beraber Module Federation devreye girdi. Bu yapı runtime’da çalışır, bağımlılık paylaşımını otomatize eder ve versiyon uyuşmazlığı durumunda akıllı bir müzakere (negotiation) yapar. 2024'te çıkan Module Federation 2.0 ise bu temeli bir adım öteye taşıdı; artık Webpack’ten bağımsız, kendi runtime’ı olan, Rspack/Vite/Rollup gibi bundler’larla çalışan bir mimari olarak karşımızda.
Webpack 5 de Module Federation ilk tanıtıldığında, Frontend Dünyasında bir devrim olarak lansmanları yapılmıştı, o günleri çok iyi hatırlıyorum 🥹
🧩 Host, Remote ve Temel Terminoloji
★ Host (Konsumer): Başka uygulamaların modüllerini tüketen ana uygulama. Micro frontend terminolojisinde buna main app veya shell de diyoruz, bazı kaynaklarda container olarak da geçiyor.
★ Remote (Provider): Kendi component’lerini dışarı açan, host tarafından çağrılan bağımsız uygulama. Genelde micro app’lerin her biri bir remote’dur.
★ exposes: Remote’un “hangi modüllerimi dışarıya açıyorum” listesini tanımladığı konfigürasyon alanı. Örneğin './Button': './src/Button.jsx' dediğinizde host, 'remote/Button' olarak import edebilir.
★ remotes: Host’un “hangi remote’lardan modül tüketeceğim” listesi. Her remote’un manifest URL’i burada tanımlanır.
★ shared: Host ve remote’lar arasında paylaşılacak bağımlılıklar (React, ReactDOM, lodash vb.). singleton: true ile işaretlendiğinde tüm uygulamada tek instance garanti edilir.
★ remoteEntry.js / mf-manifest.json: Remote’un “benim hangi modüllerim var, bunlar nerede yaşıyor, hangi shared bağımlılıklara ihtiyaçları var” bilgisini içeren giriş dosyası. Host sayfa yüklendiğinde önce bu manifest’i indirir, ardından ihtiyaç duyulan modülün gerçek kodunu talep eder.
💡 Module Federation 2.0 ile birlikte mf-manifest.json protokolü geldi. Bu yeni format runtime'a zengin metadata sağlar ve dynamic type hint, devtool gibi özellikleri mümkün kılar. Klasik remoteEntry.js hala destekleniyor.
🛠️ Adım Adım Kurulum
Şimdi iki uygulamalı minimal bir örnek kuralım.
products-app (remote) bir ürün listesi component’i dışa açacak, shell-app (host) bu component’i kullanacak.
Adım 1: Paketleri Kuralım
Module Federation 2.0 @module-federation/enhanced paketi üzerinden kullanılıyor
npm install @module-federation/enhanced
npm install -D webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader ★ @module-federation/enhanced temel paket, Webpack ve Rspack için ModuleFederationPlugin burada yaşıyor.
★ Klasik webpack/lib/container/ModuleFederationPlugin hala çalışır ama 2.0'ın dynamic type hints, devtool ve manifest özellikleri için enhanced paketini kullanmak gerekli.
Adım 2: Remote (products-app) Konfigürasyonu
Remote tarafında exposes ile hangi modülleri paylaşacağımızı, shared ile hangi bağımlılıkların ortak olacağını belirtiyoruz:
// products-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {
ModuleFederationPlugin,
} = require('@module-federation/enhanced/webpack');
module.exports = {
mode: 'development',
entry: './src/index.js',
devServer: { port: 3001 },
output: {
publicPath: 'http://localhost:3001/',
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ModuleFederationPlugin({
name: 'products_app',
filename: 'remoteEntry.js',
exposes: {
'./ProductList': './src/ProductList',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}; ★ name: 'products_app' → Remote'un global kimliği, host tarafında bu isimle referans vereceğiz.
★ filename: 'remoteEntry.js' → Manifest dosyasının adı. Host bu dosyayı indirerek remote'u tanıyacak.
★ exposes → ./src/ProductList component’ini './ProductList' adıyla dışarıya açıyoruz.
★ shared → React ve ReactDOM'u singleton olarak işaretliyoruz. requiredVersion ile hangi versiyon aralığını kabul ettiğimizi belirtiyoruz.
★ publicPath → Remote'un production'da hangi URL'den sunulacağını söylüyor. Bu ayar yanlış olursa chunk'lar yanlış yerden çekilmeye çalışılır.
Şimdi dışarı açtığımız component’i yazalım:
// products-app/src/ProductList.jsx
import React from 'react';
const products = [
{ id: 1, name: 'Mekanik Klavye', price: 1200 },
{ id: 2, name: 'Ergonomik Mouse', price: 450 },
];
export default function ProductList() {
return (
<div>
<h2>Ürünler</h2>
<ul>
{products.map((p) => (
<li key={p.id}>
{p.name} — {p.price}₺
</li>
))}
</ul>
</div>
);
} Bir önemli detay daha: Remote’un entry dosyası async boundary kullanmalı. Yani src/index.js doğrudan React'i mount etmek yerine, bootstrap.js'i dynamic import ile çağırmalı:
// products-app/src/index.js
import('./bootstrap'); // products-app/src/bootstrap.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import ProductList from './ProductList';
const root = createRoot(document.getElementById('root'));
root.render(<ProductList />); ★ Bu dynamic import, Webpack’e shared bağımlılıkları başlatması için gerekli zamanı tanır.
★ Eğer bootstrap ayrı dosyaya alınmazsa “Shared module is not available for eager consumption” hatası alırsınız. En sık yapılan hatalardan biridir.
Adım 3: Host (shell-app) Konfigürasyonu
Host tarafında remotes listesi ile hangi remote'a bağlanacağımızı tanımlıyoruz:
// shell-app/webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {
ModuleFederationPlugin,
} = require('@module-federation/enhanced/webpack');
module.exports = {
mode: 'development',
entry: './src/index.js',
devServer: { port: 3000 },
output: {
publicPath: 'http://localhost:3000/',
},
plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new ModuleFederationPlugin({
name: 'shell',
remotes: {
products_app: 'products_app@http://localhost:3001/remoteEntry.js',
},
shared: {
react: { singleton: true, requiredVersion: '^18.0.0' },
'react-dom': { singleton: true, requiredVersion: '^18.0.0' },
},
}),
],
}; ★ remotes içindeki değer formatı: {remoteName}@{url}. Sol taraf remote'un kendi konfigürasyonunda verdiği name değeriyle eşleşmeli.
★ Host da remote da aynı shared bağımlılıkları tanımlamalı. Sadece host’ta tanımlamak yeterli değil, yanlış versiyon yüklenme riski doğar.
Adım 4: Remote Component’ini Host’ta Kullanmak
Host’ta remote componenti React.lazy + Suspense ile lazy-load ediyoruz çünkü remote kodu runtime'da indiriliyor:
// shell-app/src/App.jsx
import React, { Suspense, lazy } from 'react';
const ProductList = lazy(() => import('products_app/ProductList'));
export default function App() {
return (
<div>
<header>
<h1>Shell App 🏠</h1>
</header>
<main>
<Suspense fallback={<div>Ürünler yükleniyor...</div>}>
<ProductList />
</Suspense>
</main>
</div>
);
} ★ import('products_app/ProductList') → Webpack bunu gerçek bir network isteğine çevirir. remoteEntry.js indirilir, sonra ilgili chunk çekilir.
★ Suspense fallback'i, remote JavaScript'i indirilirken kullanıcıya gösterilecek yükleme durumu. Production'da skeleton component koymak daha iyi bir kullanıcı deneyimi sağlar.
★ Host’un src/index.js dosyası da async boundary gerektirir, aynen remote'ta olduğu gibi.
Her iki uygulamayı ayrı terminallerde ayağa kaldırdığınızda (npm start ile her biri kendi portunda), localhost:3000'da shell app'i açtığınızda products_app'in ürün listesini göreceksiniz. Tebrikler, ilk Module Federation entegrasyonunuzu başarıyla tamamladınız. 🎉
📦 Shared Dependencies ve Singleton
Module Federation kurulumunda en çok kafa karıştıran konu shared bağımlılıkların nasıl çalıştığıdır. Özellikle singleton: true flag’ının neden kritik olduğu üzerinde duralım.
React ve ReactDOM gibi kütüphaneler global internal state tutar. Component tree (ağacı), hook state’leri, context değerleri hep aynı React instance’ı üzerinden yönetilir. Aynı sayfada iki farklı React kopyası çalışırsa:
★ Context değerleri karşı tarafta görünmez.
★ Hook’lar “invalid hook call” hatası verir.
★ Ref’ler beklenmedik şekilde null döner.
★ DevTools’ta iki ayrı tree (ağaç) gözükür.
singleton: true dediğimizde Module Federation runtime'ı devreye girer ve şu kararı alır: "Sayfada bu kütüphaneden zaten yüklü bir instance var mı? Varsa, versiyonu requiredVersion ile uyumlu mu? Uyumluysa onu kullan, yoksa fallback'e git."
shared: {
react: {
singleton: true,
requiredVersion: '^18.0.0',
strictVersion: false,
},
} ★ singleton: true → Yalnızca tek instance yükle.
★ requiredVersion: '^18.0.0' → Kabul edilebilir semver (Semantic Versioning) aralığı.
★ strictVersion: false → Versiyon eşleşmezse uyarı ver ama yüklemeyi durdurma. true olursa runtime error fırlatır.
Ortak kütüphaneniz React değilse ve global state tutmuyorsa (örneğin lodash, date-fns), singleton zorunlu değildir. Bu durumlarda versiyon bazlı paylaşım yeterlidir, iki farklı versiyonun aynı anda yüklenmesinde bir sakınca yoktur.
⚠️ Production’a Çıkarken Dikkat Edilecekler
Yerel ortamda çalışan bir Module Federation kurulumu, production’a çıkarken bazı ek detaylar gerektirir. Gelin sık karşılaşılan sorunları tek tek ele alalım.
CORS Ayarı
Remote’un sunulduğu domain ile host’un domain’i farklıysa (ki production’da genelde öyledir), remote sunucusu Access-Control-Allow-Origin header'ını doğru şekilde göndermelidir. CDN üzerinden servis ediyorsanız CloudFront, Cloudflare veya S3 üzerinde bu ayarı açmanız gerekir.
publicPath Dinamik Olmalı
Remote’un publicPath değeri build time'da sabitlenirse, farklı ortamlarda (staging, production) chunk URL'leri yanlış olur. Çözüm, publicPath: 'auto' kullanmak veya runtime'da environment'a göre belirlemektir:
output: {
publicPath: 'auto',
}, ★ auto → Webpack, document.currentScript.src üzerinden publicPath'i otomatik tespit eder. Çoğu durumda en güvenli seçenektir.
Versiyon Uyumsuzluklarına Hazırlıklı Olmak
Bir ekip React 18.2'ye yükselirken diğer ekip 18.0'da kalmış olabilir. strictVersion davranışını ekibinizle net konuşun, gerekirse requiredVersion aralığını geniş tutun ama kritik güncellemeleri takip için monitoring kurun.
CDN Cache Stratejisi
remoteEntry.js dosyası her deploy'da güncellenir ama içerdiği hash'li chunk isimleri sayesinde asıl kod dosyaları cache'lenebilir. remoteEntry.js'i kısa cache (veya no-cache), chunk dosyalarını ise uzun cache ile servis edin:
remoteEntry.js → Cache-Control: no-cache
chunks/*.js → Cache-Control: public, max-age=31536000, immutable Error Boundary Kullanmak
Bir remote network hatası veya deploy sırasında geçici bir sorundan dolayı yüklenemezse, tüm host uygulaması çökmemeli. Her remote’u Error Boundary ile sarın:
<ErrorBoundary fallback={<div>Ürünler şu an yüklenemiyor</div>}>
<Suspense fallback={<Skeleton />}>
<ProductList />
</Suspense>
</ErrorBoundary> Module Federation 2.0 ayrıca runtime seviyesinde errorLoadRemote hook'u sunuyor, bu sayede remote yüklenmesi başarısız olduğunda fallback stratejisi tanımlayabiliyorsunuz.
👀 Demo
Makalede anlattığım örneğin Github Reposu 👇
https://github.com/yasinatesim/react-module-federation-webpack-demo
📬 Geri Bildirim
Makaleyi yazarken, kaynakları belirleme ve araştırma için kendi notlarımı, yazım denetimi ve ek araştırma için Claude Opus 4.7 modelini kullandım. Resimleri üretmek için ise Gemini 3 Pro Preview 2k (Nano Banana Pro) modelini kullandım.
Yazı ile ilgili tavsiye, öneri, eleştirileri dikkate alıyorum. İletişime geçmek isterseniz bana websitemdeki sosyal medya adreslerimden veya Linkedin üzerinden ulaşabilirsiniz.
Sevgiyle kalın, Yasin 🤗
📚 Makaleyi Yazarken Kullandığım Kaynaklar
- Module Federation Resmi Dokümantasyonu — Module Federation 2.0'ın resmi dokümantasyonu; konfigürasyon, runtime API ve best practice’ler için ana kaynak.
- Module Federation 2.0 Announcement — MF 2.0'ın tanıtıldığı resmi blog post; runtime decoupling ve yeni özelliklerin detayları.
- MF 2.0 Stable Release Blog — 2.0 stable sürümünün performans iyileştirmeleri ve tree-shaking kapasitesinin anlatımı.
- Webpack Plugin Guide — @module-federation/enhanced/webpack paketinin Webpack ile entegrasyonu için resmi rehber.
- Webpack Module Federation Concepts — Webpack’in kendi dokümantasyonu; async boundary ve eager consumption konularında kritik bilgi içeriyor.
- ModuleFederationPlugin Reference — shared, singleton, requiredVersion parametrelerinin detayları.
- module-federation/core GitHub — Ana repository; release notes ve topluluk tartışmaları için referans.
- Module Federation Examples — Resmi örnek repository; farklı framework ve senaryolar için hazır konfigürasyonlar.
- @module-federation/nextjs-mf — Next.js için resmi Module Federation plugin’i.
- @module-federation/vite — Vite için resmi Module Federation plugin’i; MF 2.0 runtime ile uyumlu.
- Rspack Module Federation Guide — Rspack’in Module Federation 2.0 entegrasyonu.
- Micro Frontend Mimarisi: React Örnekleriyle — Bu yazının teorik alt yapısı; micro frontend mimarisinin genel prensipleri.
- https://semver.org/ — Semantic Versioning