In a monolithic frontend architecture, a single codebase handles the entire user interface. While this can simplify initial development, it can become complex as the application grows:
Micro-frontends can help when you start facing those problems. Unlike Web Components, coming with limited inter-framework communication and lifecycle management challenges, Vite based micro-frontends allow developers to work with differents frameworks. They provide flexibility in tooling, better state management, and more robust integration options. In a real life software growing through years, being able to handle multiple frameworks can be a smooth way to migrate from an old one to a new one whenever it's needed.
In this article, we’ll create a micro-frontend setup using Vite as our build tool and combine Vue.js, Angular, and React components into a unified experience. The example? A modular news portal, where each framework handles a specific section.
This modular news portal will have :
In a real-world example, separating news management across multiple technologies wouldn’t be ideal, but it serves our example well.
In micro-frontend architecture, the Shell acts as the container for the micro-frontends. It has 3 main features :
The Shell in our setup will use Vite and load ESM modules dynamically:
host/
├── index.html # Main entry point
├── main.js # Loads the micro-frontends
├── vite.config.js
├── package.lock.json
├── package.json
apps/ # Contains individual micro-frontends
├── header/
├── src/
├── components/
├── Header.vue
├── main.js
├── vite.config.js
├── package.lock.json
├── package.json
├── trending/
├── src/
├── components
├── Trending.jsx
├── main.jsx
├── eslint.config.js
├── package.lock.json
├── vite.config.js
├── highlights/
Let's build it !
We start by initializing a Vite workspace for our host and micro-frontends.
mkdir news-portal && cd news-portal
npm init vite@latest host --template vanilla
Let’s organize the project to separate each micro-frontend:
mkdir -p apps/header apps/trending apps/highlights
Now, let's create our simple index.html with the components architecture inside our DOM :
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>News Portal</title>
</head>
<body>
<div id="header"></div>
<div id="trending"></div>
<div id="highlights"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
Now, let's create our main.js file that is responsible of mounting the micro-frontends (note that imports won't work until we build our micro-frontends):
// main.js
import { mount as mountHeader } from '../apps/header/dist/header.js';
import { mount as mountTrending } from '../apps/trending/dist/trending.js';
import { mount as mountHighlights } from '../apps/highlights/dist/highlights.js';
mountHeader(document.querySelector('#header'));
mountTrending(document.querySelector('#trending'));
mountHighlights(document.querySelector('#highlights'));
Then, we create the Vite configuration inside vite.config.js to enable the Vite server :
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
server: {
port: 3000,
open: true,
},
});
Start the Shell to be ready to serve :
cd host
npm install
npm run dev
And here we are : we've successfully created our Shell and it's ready to serve our future micro-frontends. Now, let's create them !
Let's create our Header folder inside apps and navigate into it:
cd apps
npm init vite@latest header --template vue
cd header
npm install
Inside src/components/Header.vue
, create a simple header with navigation and for example a search bar:
<template>
<header class="header">
<nav>
<ul>
<li><a href="#">Home</a></li>
<li><a href="#">World</a></li>
<li><a href="#">Tech</a></li>
<li><a href="#">Sports</a></li>
</ul>
</nav>
<input type="text" placeholder="Search news..." />
</header>
</template>
<script>
export default {
name: 'Header',
};
</script>
<style scoped>
.header {
display: flex;
justify-content: space-between;
padding: 1em;
background: #333;
color: white;
}
nav ul {
display: flex;
list-style: none;
}
nav ul li {
margin-right: 1em;
}
input {
padding: 0.5em;
}
</style>
We need a src/main.js
to mount the component :
import { createApp } from 'vue';
import Header from './components/Header.vue';
export function mount(el) {
createApp(Header).mount(el);
}
Configure vite.config.js to expose this app as a library :
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
build: {
lib: {
entry: './src/main.js',
name: 'Header',
fileName: 'header',
},
},
})
Finally, build the micro-frontend to generate the dist folder :
cd apps/header
npm run build
You should now be able to see your Header served in your Shell. It happens because we told to our Shell to serve the dist folder of our Header and we generated it with the npm build
command.
Let's create our Trending folder inside apps and navigate into it:
cd apps
npm init vite@latest trending --template react
cd trending
npm install
Add a Trending component in src/components/Trending.jsx
import React from 'react';
const Trending = () => {
const articles = [
{ id: 1, title: "React 18 Released", summary: "Learn what's new in React 18." },
{ id: 2, title: "AI Revolution", summary: "How AI is transforming industries." },
];
return (
<section className="trending">
<h2>Trending News</h2>
<ul>
{articles.map((article) => (
<li key={article.id}>
<h3>{article.title}</h3>
<p>{article.summary}</p>
</li>
))}
</ul>
</section>
);
};
export default Trending;
We need a src/main.jsx
to mount the component :
import React from 'react';
import ReactDOM from 'react-dom/client';
import Trending from './components/Trending';
export function mount(el) {
const root = ReactDOM.createRoot(el);
root.render(<Trending />);
}
Configure vite.config.js
:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
lib: {
entry: './src/main.jsx',
name: 'Trending',
fileName: 'trending',
},
},
});
Build to generate the dist folder :
npm run build
And here we are ! We now have our second micro-frontend served inside our Shell, under our Header.
Edit: I haven't been able to make Vite works with Angular 19 without module federation, custom element or widget yet. I'm currently trying to find the best approach between the 3 to propose you the most efficient in a later edit of this post.
While the approach described above focuses on combining microfrontends during the build step, for production, you might want to embrace runtime federation. This approach offers true independence for microfrontends, allowing each module to be deployed separately and updated independently.
Each microfrontend can be hosted on its own server or domain. For example:
The shell application dynamically loads microfrontends at runtime using import() or similar methods. Here’s how the shell could dynamically load both the Header and Trending modules from their respective URLs, listed in a microfrontend manifest json document, in the Shell main.js
:
Manifest of microservices (ex: https://news-portal.com/microfrontends.json) :
[
{
"name": "Header",
"url": "https://header.news-portal.com/dist/header.js"
},
{
"name": "Trending",
"url": "https://trending.news-portal.com/dist/trending.js"
}
]
Shell main.js
:
export default {
data() {
return {
modules: [],
};
},
async mounted() {
try {
const response = await fetch('https://news-portal.com/microfrontends.json');
const microfrontends = await response.json();
for (const mfe of microfrontends) {
const module = await import(mfe.url);
this.modules.push({
name: mfe.name,
component: module.default,
});
}
} catch (error) {
console.error('Failed to load microfrontends:', error);
}
},
template: `
<div>
<h1>Shell App</h1>
<div v-for="module in modules" :key="module.name">
<component :is="module.component" />
</div>
</div>
`,
};
To avoid duplicating common dependencies like Vue, we could use peerDependencies
in our package.json
and externalize them in the Vite build process. We can configure it in vite.config.js
:
import { defineConfig } from 'vite';
export default defineConfig({
build: {
rollupOptions: {
external: ['vue'], // Prevent bundling Vue with the module
},
},
});
This ensures that the Shell provides the dependency (e.g., Vue), and both Header and Trending modules rely on the same instance.
During local development, micro-frontends hosted on different ports may face CORS issues due to browser security policies
For production, configure your web server to allow cross-origin requests:
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept';
Minimize CORS requirements by deploying all micro-frontends and the Shell under the same domain or subdomain structure (e.g., https://news-portal.com/header, https://news-portal.com/trending).
Github to the code : Code Repository
Congratulations, you've setup your first MicroFrontend architecture with Vite. For those who already used web components in the past, you may find it really simpler and useful. I hope it will be a good help to set up your first MFE projects whenever you'll need to decouple the development of multiple front parts of your software.