Micro-frontend sering dibicarakan sebagai solusi untuk tim frontend besar, tetapi tidak selalu tepat untuk semua aplikasi. Pada konteks Nuxt 3, pola ini menjadi menarik ketika beberapa tim perlu mengembangkan fitur secara independen, merilis perubahan tanpa menunggu monorepo utama, dan menjaga batas domain yang jelas. Namun, implementasinya tidak sederhana, terutama ketika masuk ke urusan server-side rendering (SSR), berbagi dependency, routing, state, dan konsistensi UI.

Artikel ini membahas pendekatan implementatif untuk membangun micro-frontend dengan Nuxt 3 menggunakan Module Federation, termasuk kapan pola ini layak dipakai, bagaimana membagi aplikasi menjadi host dan remote, serta trade-off teknis yang perlu dipahami sebelum mengadopsinya.

Kapan Micro-Frontend Layak Dipakai?

Micro-frontend bukan sekadar memecah UI menjadi beberapa repository. Tujuan utamanya adalah memisahkan kepemilikan fitur berdasarkan domain bisnis agar tim dapat bekerja lebih mandiri. Pola ini layak dipertimbangkan jika beberapa kondisi berikut benar:

  • Tim frontend cukup besar dan dibagi per domain, misalnya checkout, katalog, akun, dan loyalty.
  • Siklus rilis berbeda antar fitur sehingga deploy terpusat menjadi bottleneck.
  • Batas domain jelas, sehingga dependensi antar bagian UI dapat diminimalkan.
  • Kebutuhan eksperimen atau migrasi bertahap dari aplikasi lama ke arsitektur baru.

Sebaliknya, micro-frontend cenderung berlebihan jika aplikasi masih kecil, tim hanya beberapa orang, atau masalah utama sebenarnya ada pada kode yang belum terstruktur dengan baik. Banyak kasus bisa diselesaikan dengan modular monolith, monorepo, atau package internal tanpa menambah kompleksitas runtime.

Prinsip penting: gunakan micro-frontend untuk menyelesaikan masalah organisasi dan skala delivery, bukan sekadar mengikuti tren arsitektur.

Konsep Dasar: Host dan Remote pada Nuxt 3

Pada Module Federation, aplikasi biasanya dibagi menjadi:

  • Host: aplikasi utama yang memuat dan merender remote.
  • Remote: aplikasi atau modul frontend yang mengekspose komponen, halaman, utilitas, atau fitur tertentu.

Dalam konteks Nuxt 3, host tetap menjadi shell utama aplikasi: menangani layout global, autentikasi level aplikasi, middleware inti, dan komposisi halaman. Remote biasanya mengekspose blok UI atau fitur domain, misalnya ProductGallery, CartSummary, atau halaman akun.

Secara praktis, pembagian yang sehat adalah berdasarkan domain bisnis, bukan berdasarkan jenis komponen teknis. Misalnya, “checkout” sebagai remote terpisah lebih masuk akal daripada “buttons”, “forms”, dan “cards” sebagai remote terpisah. Komponen UI dasar sebaiknya tetap menjadi package bersama, bukan remote runtime.

Contoh pembagian domain

  • Host: shell aplikasi, layout, navigasi, auth context, observability.
  • Remote katalog: listing produk, pencarian, filter.
  • Remote checkout: keranjang, alamat, pembayaran.
  • Remote akun: profil, order history, preferensi pengguna.

Struktur Implementasi Nuxt 3 dengan Module Federation

Ekosistem Nuxt 3 berjalan di atas Vite atau bundler berbasis Webpack tergantung setup yang dipilih. Module Federation secara historis berasal dari Webpack, tetapi kini tersedia juga implementasi untuk ekosistem modern melalui plugin federation. Apa pun tool yang dipakai, prinsip utamanya sama: remote mengekspose modul, host mengonsumsinya secara dinamis.

Contoh berikut menunjukkan konfigurasi yang umum dipakai dengan pendekatan federation pada build frontend. Nama plugin bisa berbeda sesuai toolchain yang dipilih, tetapi pola konfigurasinya serupa.

Konfigurasi remote

// nuxt.config.ts pada remote checkout
import { defineNuxtConfig } from 'nuxt/config'
import federation from '@originjs/vite-plugin-federation'

export default defineNuxtConfig({
  vite: {
    plugins: [
      federation({
        name: 'checkout_remote',
        filename: 'remoteEntry.js',
        exposes: {
          './CheckoutWidget': './components/CheckoutWidget.vue',
          './useCheckoutApi': './composables/useCheckoutApi.ts'
        },
        shared: {
          vue: { singleton: true, requiredVersion: '^3.0.0' },
          'vue-router': { singleton: true },
          pinia: { singleton: true }
        }
      })
    ],
    build: {
      target: 'esnext'
    }
  }
})

Konfigurasi host

// nuxt.config.ts pada host
import { defineNuxtConfig } from 'nuxt/config'
import federation from '@originjs/vite-plugin-federation'

export default defineNuxtConfig({
  vite: {
    plugins: [
      federation({
        name: 'main_host',
        remotes: {
          checkout_remote: 'https://cdn.example.com/checkout/remoteEntry.js'
        },
        shared: {
          vue: { singleton: true, requiredVersion: '^3.0.0' },
          'vue-router': { singleton: true },
          pinia: { singleton: true }
        }
      })
    ]
  }
})

Di sisi host, remote dapat dimuat secara dinamis menggunakan defineAsyncComponent agar beban awal tetap terkontrol.

<script setup lang="ts">
import { defineAsyncComponent } from 'vue'

const CheckoutWidget = defineAsyncComponent(
  () => import('checkout_remote/CheckoutWidget')
)
</script>

<template>
  <section>
    <CheckoutWidget />
  </section>
</template>

Pendekatan ini bekerja karena host tidak perlu membundel semua kode remote saat build. Host hanya memuat manifest remote saat runtime, lalu mengambil modul yang diperlukan. Inilah dasar yang memungkinkan deployment independen.

Strategi Membagi Aplikasi: Remote yang Tepat, Batas yang Jelas

Kesalahan umum dalam micro-frontend adalah memecah aplikasi terlalu granular. Jika terlalu banyak remote kecil, Anda akan mendapat overhead jaringan, koordinasi routing yang rumit, dan debugging yang melelahkan. Sebaiknya gunakan prinsip berikut:

  • Satu remote untuk satu domain bisnis utama, bukan per komponen kecil.
  • Minimalkan komunikasi sinkron antar remote.
  • Pastikan kontrak antarmuka stabil, misalnya props, events, atau API composable yang terdokumentasi.
  • Biarkan host mengontrol shell: layout global, header, footer, auth gate, error boundary.

Contoh yang sehat: remote checkout hanya bertanggung jawab pada alur checkout. Ia boleh mengekspose widget utama dan beberapa utilitas domain, tetapi tidak mengambil alih state global yang tidak relevan.

Berbagi State Tanpa Membuat Coupling Berlebihan

State sharing adalah area yang paling sering disalahpahami. Secara teknis Anda bisa berbagi store seperti Pinia sebagai singleton, tetapi ini tidak otomatis berarti semua remote harus membaca dan menulis ke store yang sama. Semakin besar state global, semakin tinggi coupling antar tim.

Pola yang disarankan

  • Host menyimpan state global minimum: sesi pengguna, token, locale, feature flag.
  • Remote menyimpan state domain lokal: misalnya state checkout hanya dikelola remote checkout.
  • Gunakan kontrak eksplisit: props, callback, custom events, atau composable bersama untuk pertukaran data yang memang diperlukan.

Contoh sederhana, host mengirimkan data pengguna ke remote sebagai props:

<CheckoutWidget :user="user" :cart-id="cartId" @completed="handleCheckoutDone" />

Jika perlu komunikasi lintas remote, pertimbangkan event bus yang sangat terbatas atau shared package untuk tipe data dan helper, bukan store bersama yang tumbuh liar. Shared store global sering nyaman di awal, tetapi dalam jangka panjang menjadi titik coupling yang menghapus manfaat micro-frontend itu sendiri.

Design System dan Konsistensi UI

Dalam arsitektur micro-frontend, design system sebaiknya dibagikan sebagai package versi, bukan sebagai remote runtime. Alasannya sederhana: komponen UI dasar harus stabil, ringan, dan bisa dites secara terisolasi. Jika button, modal, dan form control dimuat sebagai remote, latensi runtime dan potensi mismatch akan meningkat.

Praktik yang efektif

  • Buat package internal seperti @company/design-system.
  • Bagikan token desain: warna, spacing, typography, breakpoint.
  • Gunakan kontrak CSS yang konsisten, misalnya CSS variables atau utility class yang disepakati.
  • Sediakan dokumentasi visual dan aturan aksesibilitas.

Untuk menjaga konsistensi, host dapat memuat tema global, sementara remote hanya menggunakan token yang tersedia. Dengan cara ini, tim domain tetap mandiri, tetapi tampilan aplikasi tidak terasa terpecah.

Routing: Siapa yang Mengendalikan Navigasi?

Pada Nuxt 3, routing berbasis file-system adalah kekuatan utama. Namun micro-frontend menambah lapisan koordinasi. Aturan praktisnya: host mengontrol routing tingkat atas, remote mengelola sub-flow internal jika memang perlu.

Misalnya:

  • /products diarahkan ke remote katalog.
  • /checkout diarahkan ke remote checkout.
  • /account diarahkan ke remote akun.

Host dapat menyediakan halaman pembungkus yang memuat komponen remote berdasarkan route aktif. Jika remote memiliki sub-route internal, Anda bisa:

  1. Mendelegasikan sub-path ke remote melalui props atau konfigurasi internal.
  2. Memetakan route host ke komponen remote tertentu.
  3. Menghindari remote memiliki router independen penuh kecuali benar-benar dibutuhkan.

Kesalahan umum adalah membiarkan beberapa router hidup tanpa batas yang jelas. Ini bisa menyebabkan masalah sinkronisasi URL, history navigation, dan middleware auth.

Jika SEO dan SSR penting, usahakan URL utama tetap dikelola host agar alur rendering dan metadata lebih mudah dikontrol.

Version Mismatch Dependency dan Strategi Shared Dependency

Module Federation memungkinkan dependency seperti vue, pinia, atau library utilitas dibagikan sebagai shared module. Namun ini juga memunculkan risiko mismatch versi. Jika host dan remote memakai versi mayor yang tidak kompatibel, runtime bisa gagal dengan error yang sulit dilacak.

Strategi mitigasi

  • Jadikan dependency inti singleton: vue, vue-router, pinia.
  • Sinkronkan versi lewat governance, misalnya constraints di monorepo atau matriks kompatibilitas di CI.
  • Bedakan shared runtime dan shared package. Tidak semua package perlu dibagikan lewat federation.
  • Hindari berbagi terlalu banyak dependency hanya demi mengurangi ukuran bundle.

Praktik yang baik adalah membuat daftar dependency yang wajib disejajarkan lintas tim, dan dependency yang boleh berbeda versi karena tidak menjadi bagian dari runtime inti. CI juga sebaiknya memvalidasi bahwa versi vue dan library fundamental tidak menyimpang.

Tantangan SSR pada Nuxt 3

SSR adalah bagian paling menantang dari micro-frontend di Nuxt 3. Pada aplikasi tradisional, seluruh tree komponen dirender dalam satu konteks server. Pada micro-frontend, host bisa saja harus memuat remote dari sumber lain saat runtime, yang artinya ada isu tambahan: resolusi modul di server, serialisasi state, kompatibilitas environment, dan sinkronisasi hydration di browser.

Masalah yang sering muncul

  • Hydration mismatch karena hasil render server berbeda dari klien.
  • Remote tidak tersedia saat SSR karena endpoint manifest gagal diakses.
  • Perbedaan environment, misalnya kode remote mengakses window saat dirender di server.
  • Metadata halaman seperti title dan meta tag menjadi sulit dikoordinasikan.

Pendekatan pragmatis

  • Gunakan SSR penuh hanya untuk bagian yang benar-benar membutuhkan SEO atau TTFB yang baik.
  • Untuk remote tertentu, pertimbangkan client-only mount jika SSR terlalu kompleks dan fitur tidak kritikal untuk SEO.
  • Pastikan komponen remote aman untuk SSR: cek akses ke API browser, gunakan guard seperti process.client atau utilitas setara.
  • Siapkan fallback UI jika remote gagal dimuat.

Contoh fallback sederhana:

<ClientOnly>
  <CheckoutWidget />
  <template #fallback>
    <div>Memuat modul checkout...</div>
  </template>
</ClientOnly>

Ini bukan solusi universal, tetapi sering menjadi kompromi realistis. Jika seluruh aplikasi sangat bergantung pada SSR, evaluasi lebih dalam apakah federation runtime benar-benar cocok, atau lebih baik memakai integrasi build-time/package-based untuk area tertentu.

Deployment Independen dan Operasional

Salah satu alasan utama mengadopsi micro-frontend adalah deployment independen. Remote dapat dirilis ke CDN atau origin masing-masing tanpa membangun ulang host, asalkan kontrak publik tidak berubah secara breaking.

Strategi deployment yang umum

  • Host memuat remote via URL manifest tetap, misalnya /checkout/remoteEntry.js.
  • Versi remote dipublikasikan ke path immutable, lalu alias latest diperbarui setelah verifikasi.
  • Gunakan cache-control yang tepat: manifest singkat, asset hashed panjang.
  • Sediakan rollback cepat dengan mengembalikan pointer manifest ke versi sebelumnya.

Praktik yang matang biasanya melibatkan pipeline CI/CD per remote, smoke test setelah publish, dan observability yang bisa menunjukkan remote mana yang sedang aktif di browser pengguna.

Menjaga DX Tetap Baik untuk Tim Lintas Domain

Micro-frontend mudah merusak developer experience jika setiap tim harus menebak kontrak runtime sendiri. Agar produktivitas tetap tinggi, buat aturan yang sederhana namun tegas:

  • Dokumentasikan kontrak expose: nama modul, props, event, dependency yang diharapkan.
  • Sediakan local fallback/mock remote agar host bisa dikembangkan tanpa menunggu environment penuh.
  • Gunakan shared types lewat package internal untuk event payload, model data, dan interface utama.
  • Tambahkan contract test agar perubahan breaking terdeteksi sebelum deploy.
  • Standarkan observability: logging, tracing, dan error boundary lintas remote.

Jangan abaikan dokumentasi. Pada arsitektur seperti ini, dokumentasi kontrak sama pentingnya dengan kode.

Performa, Trade-off, dan Kesalahan yang Sering Terjadi

Module Federation bukan gratis. Anda mendapatkan independensi deployment, tetapi membayar dengan kompleksitas runtime. Beberapa trade-off yang perlu diawasi:

  • Lebih banyak request jaringan untuk manifest dan chunk remote.
  • Potensi duplikasi dependency jika shared config tidak konsisten.
  • Debugging lebih sulit karena error bisa berasal dari host, remote, CDN, atau mismatch versi.
  • Observability wajib, bukan opsional.

Tips performa

  • Muat remote secara lazy hanya saat dibutuhkan.
  • Kelompokkan remote berdasarkan domain besar, jangan terlalu granular.
  • Gunakan prefetch atau preload hanya untuk remote yang benar-benar sering diakses.
  • Audit bundle shared dependency secara berkala.
  • Tambahkan fallback UI dan timeout agar kegagalan remote tidak memblokir seluruh halaman.

Debugging tips

  • Periksa apakah remoteEntry.js benar-benar dapat diakses dari browser dan server.
  • Bandingkan versi dependency inti antara host dan remote.
  • Cek hydration warning di console jika menggunakan SSR.
  • Pastikan komponen remote tidak mengakses API browser saat dirender di server.
  • Aktifkan source map dan log versi build pada setiap remote.

Penutup

Micro-frontend dengan Nuxt 3 dan Module Federation bisa menjadi arsitektur yang sangat berguna ketika organisasi membutuhkan otonomi tim, rilis independen, dan batas domain yang jelas. Namun manfaat itu datang bersama kompleksitas nyata: routing, state sharing, version mismatch, deployment, observability, dan terutama SSR.

Pendekatan yang paling aman adalah memulai dari domain yang benar-benar terpisah, menjaga host sebagai shell yang stabil, membagikan design system sebagai package, dan menahan diri agar state global tidak menjadi tempat semua hal dicampur. Jika dilakukan dengan disiplin kontrak, governance dependency, dan strategi deployment yang baik, micro-frontend dapat mempercepat delivery tanpa mengorbankan maintainability dan performa.

Jika kebutuhan Anda belum sampai ke sana, modular monolith atau package internal sering kali menjadi pilihan yang lebih sederhana dan lebih murah untuk dioperasikan. Pilih arsitektur berdasarkan masalah yang ingin diselesaikan, bukan berdasarkan nama polanya.