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:
- Scaling: Large teams working in a monolithic repo may face merge conflicts, slower CI/CD pipelines, and difficulty with dependencies
- Independency: sometimes working on shared stuff can impact other teams
- Resilience: a failure can bring down the whole application
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.
Modular News Portal
This modular news portal will have :
- a Header made with Vue.js: with a simple navigation bar
- a Trending made with React: with the last articles to show
- an Highlights made with Angular: with the most popular articles to show
In a real-world example, separating news management across multiple technologies wouldn’t be ideal, but it serves our example well.
Building the Shell
In micro-frontend architecture, the Shell acts as the container for the micro-frontends. It has 3 main features :
- Coordination: The Shell is responsible for loading and rendering the different micro-frontends into designated areas of the application
- Orchestration: It handles global concerns like navigation, shared styles, shared state, data...
- Entry Point: This is what the users loads first in their browser. It can also provide fallbacks if one of the micro-frontends fails to load.
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 !
Building the Header with Vue 3
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.
Building the Trending section with React 18
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.
Building Highlights with Angular 19
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.
Bonus: Runtime Federation with Vite
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:
- https://header.news-portal.com for Header
- https://trending.news-portal.com for Trending
Dynamic Loading in the Shell Application
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>
`,
};
Sharing Dependencies
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.
Benefits of Runtime Federation
- Independent Deployments: Each module can be updated without redeploying the entire application.
- Scalability: Teams can work on different microfrontends autonomously.
- Dynamic Composition: New modules can be added or updated without touching the shell or other microfrontends.
Common pitfalls
Port Conflicts
- Assign a unique port to each micro-frontend in vite.config.js or angular.json to avoid conflicts
Shared Libraries
- Use a shared CDN for common libraries to reduce duplication and bundle sizes
- Ensure version compatibility to avoid runtime errors
CORS problems
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 link
Github to the code : Code Repository
Conclusion
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.
Top comments (36)
This is great for SPAs, but if you guys just want a traditional website with different frameworks for functionality, try Astro instead
When choosing between Vite and Astro for microfrontends, it depends on your project’s needs.
Imagine you're building a SaaS dashboard for analytics with microfrontends:
Where Astro is Limited:
Why Vite Excels:
Conclusion
Astro is ideal for static pages, but for the dynamic, interactive SaaS dashboard core, Vite’s performance, flexibility, and robust runtime support can make it a better choice.
100% recommend Astro over anything else these days. Unless you need something specific from one of the other *kit frameworks
dev.to/mairouche/comment/2ka02
Sorry, but I don't see the "micro frontend architecture". There are some core properties that are unanswered (or maybe I could not see / find it):
Generally, if you look at things such as Module Federation, Native Federation, Piral, ... these solutions give you something that is distributed from the beginning, i.e., where you (by default) have runtime composition.
Having team autonomy (which is the most important aspect of a micro frontends architecture) is only possible with independent deployment processes. So how would I update, e.g., the trending module, without updating the whole application? How could I add another module without having to redeploy the application?
Sorry if my comment comes across negative - it is meant as an encouragement; not as a letdown.
Thank you for your comment! Every feedback is welcome.
You’re absolutely right that runtime composition and independent deployments are very important. My article primarily focuses on the get started, where modules are combined via
dist
directories for simplicity of the article and quick setup.However, for production, Vite can indeed support runtime federation by leveraging:
I’ll update the article to clarify that the dist approach is meant to onboard, while runtime federation is better suited for production. Thanks again for your valuable feedback, it helps make the article more complete! 😊
I added the bonus section to the article to explain how :) Thank you
very good tutorial, but angular v19 do not have .\app\app.module.ts, in dist folder, vue generated header.umd.cjs file, react do not generate trending.umd.js.
I passed everything in ESM since it's more standard now. Thank you for the feedback <3
current status - 2024-12-11 - base concept works fine
I added the repository with the overall code working (except for Angular) at the end of the article : github.com/mairouche/mfe-news-portal
Thanks, this is kick-ass.
I have a doubt, how would u share data across these 2 components? Vue to react and vice versa
With Redux for state management for example which is accessible from Vue, React or even Angular.
With RxJS for cross-component events since it's also compatible with each lib/framework.
You can also have shared data in the Shell like following for example :
Absolutely loved this post!!
Thank you very much <3
Thanks for sharing.
This is amazing.
This is exactly what's happening in Astro behind the hood.
Correct me if I'm wrong.
To be Honest I discovered Astro with a previous comment and I'll deep dive into it. It looks like the same with Astro being maybe a bit more opinionated and closer to a framework than a library. But I'll look for it to see if I well understood
After deep diving into astro and trying it (only for 2 days so not that much background at this stage), this is what I conclude :
dev.to/mairouche/comment/2ka02
How does the router work in this case?
This is a good question. I was affraid that the article would be too long if I deep dived into routing, state, refs etc...
I'll answer here then.
In the Shell, you can use a vue or react router (what you prefer) to route to any kind of component. For example :
Once the microfrontend is loaded throught the global Shell router, it uses its own internal router if it has one.
So a good practice would be to keep the Global Shell router just load the micr-frontends and then, each micro-frontends having its own dedicated standalone router.
Thanks for this post i will try this with angular
Absolutely agree with your points. This is a game-changer!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.