DEV Community

Cover image for Setup a Micro-Frontend architecture in 15min with Vite!
Meidi Airouche
Meidi Airouche

Posted on

Setup a Micro-Frontend architecture in 15min with Vite!

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/
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

Let’s organize the project to separate each micro-frontend:

mkdir -p apps/header apps/trending apps/highlights
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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'));
Enter fullscreen mode Exit fullscreen mode

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,
  },
});
Enter fullscreen mode Exit fullscreen mode

Start the Shell to be ready to serve :

cd host
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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',
    },
  },
})
Enter fullscreen mode Exit fullscreen mode

Finally, build the micro-frontend to generate the dist folder :

cd apps/header
npm run build 
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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 />); 
}
Enter fullscreen mode Exit fullscreen mode

Configure vite.config.js :

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: './src/main.jsx',
      name: 'Trending',
      fileName: 'trending',
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

Build to generate the dist folder :

npm run build
Enter fullscreen mode Exit fullscreen mode

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.

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';
Enter fullscreen mode Exit fullscreen mode

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 (20)

Collapse
 
ozzythegiant profile image
Oziel Perez

This is great for SPAs, but if you guys just want a traditional website with different frameworks for functionality, try Astro instead

Collapse
 
mahmoudalaskalany profile image
Mahmoud Alaskalany

Thanks for this post i will try this with angular

Collapse
 
griffinrivera profile image
Griffin Rivera

Thanks for sharing.

Collapse
 
ciphernutz profile image
Ciphernutz IT Services

Absolutely agree with your points. This is a game-changer!

Collapse
 
han_yang_20969b18accbe79e profile image
Han Yang

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.

Collapse
 
mairouche profile image
Meidi Airouche

I passed everything in ESM since it's more standard now. Thank you for the feedback <3

Collapse
 
han_yang_20969b18accbe79e profile image
Han Yang • Edited

current status - 2024-12-11 - base concept works fine

Image description

Thread Thread
 
mairouche profile image
Meidi Airouche

I added the repository with the overall code working (except for Angular) at the end of the article : github.com/mairouche/mfe-news-portal

Collapse
 
farseenmanekhan1232 profile image
Mohammad Farseen Manekhan

This is amazing.
This is exactly what's happening in Astro behind the hood.
Correct me if I'm wrong.

Collapse
 
mairouche profile image
Meidi Airouche

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

Collapse
 
lwz7512 profile image
liwenzhi

Couldnt integrate angular app, got error:

Uncaught SyntaxError: The requested module '/apps/highlights/dist/main.js' does not provide an export named 'mount' (at main.js:3:10)

has anyone make it run successful?

Collapse
 
mairouche profile image
Meidi Airouche

Depends on the version. Try with this in src/main.js:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

function mount(el: HTMLElement) {
  const bootstrap = async () => {
    const appElement = document.createElement('app-highlights');
    el.appendChild(appElement);
    await platformBrowserDynamic().bootstrapModule(AppModule);
  };

  bootstrap().catch(err => console.error(err));
}

export { mount };
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lwz7512 profile image
liwenzhi

got an abnormal main.js file under dist folder:

"use strict";(self.webpackChunkhighlights=self.webpackChunkhighlights||[]).push([[792],{429:(e,o,_)=>{}},e=>{e(e.s=429)}]);
Enter fullscreen mode Exit fullscreen mode

Do you mind sharing your full project code? this is really interesting...i have never seen a solution like this...

Thread Thread
 
mairouche profile image
Meidi Airouche

I update to last versions and share it tomorrow. I ping you in comment when it's done

Thread Thread
 
mairouche profile image
Meidi Airouche • Edited

@lwz7512 Indeed, I was using module federation with old Angular versions. I will find out what's the best approach between, widgets, module federation or web component and propose it in an edit of the article.

Collapse
 
getsetgopi profile image
GP

Integrating multiple technologies within a micro frontend architecture is generally regarded as a suboptimal design choice. This approach can lead to significant performance issues, as it necessitates loading numerous technology libraries alongside each component, resulting in a bloated and resource-intensive application. Moreover, the complexity introduced by this multi-technology setup can make maintenance and management exceptionally challenging.

A more streamlined and efficient approach is to standardize on a single library or framework across the micro frontend ecosystem. This strategy offers several advantages:

  1. Improved performance due to reduced overhead from multiple libraries
  2. Simplified codebase that's easier to maintain and update
  3. Enhanced consistency in development practices and user experience
  4. Better team collaboration and knowledge sharing
  5. Reduced learning curve for new developers joining the project
  6. More straightforward integration and communication between micro frontends

By focusing on a unified technology stack, development teams can create more cohesive, performant, and maintainable micro frontend applications that align better with the core principles of this architectural pattern.

Collapse
 
mairouche profile image
Meidi Airouche • Edited

Agree. This is why I explained in the article that it can be a solution (temporary one) to shift from a framework to another smoothly here :

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.

For example, in my experience, we transitionned form Angular to Vue step by step thanks to this approach. I'm not saying it's a desirable design but it's an existing one. Used in the appropriate context, it can bring value.

Collapse
 
sirserje profile image
Serhii Viazkov

did you have any luck with precious angular versions , eg 18?

Collapse
 
mairouche profile image
Meidi Airouche

I succeed with module federation from Angular 16- but it's not that simple. I'm still testinfg things but Vue and React are Libraries whereas Angular, as a Framework, has it's own event, state, data management etc... So it's not that easy.

Collapse
 
hieu_buihuu_3fe912f1ad47 profile image
Kevin.CK

Thanks