Building a Micro Frontend Architecture with Qwik, Angular, React, and Rust#
Micro frontend architecture is a practical way to develop scalable and modular web applications. By breaking down a monolithic frontend into smaller, independently deployable modules, teams can work more efficiently and scale their applications with ease.
In this example, Qwik acts as the shell application. Angular and React are built as separate micro frontends, exposed as Web Components, and loaded into the shell at runtime. A small Rust WebAssembly module is included as an optional non-UI helper for CPU-bound work. The shell owns shared state, passes it down through custom element attributes, and listens for messages from the micro frontends through a small DOM event contract.
Each micro frontend builds into qwik-micro-frontend/public/mfes/. The optional Rust WASM package is emitted there too, so the shell can serve every independently built piece from one public asset folder. The root package.json orchestrates the build so Angular, React, Rust WASM, and the Qwik shell can be built with one command.
// qwik-micro-frontend/src/routes/index.tsx
import{$,component$,useSignal,useVisibleTask$}from'@builder.io/qwik';constassetBase=import.meta.env.BASE_URL;constassetUrl=(path: string)=>{constbase=assetBase.endsWith('/')?assetBase:`${assetBase}/`;return`${base}${path.replace(/^\//,'')}`;};constscripts=newMap<string,Promise<void>>();constloadScript=(src: string)=>{if(scripts.has(src)){returnscripts.get(src);}constpromise=newPromise<void>((resolve,reject)=>{constscript=document.createElement('script');script.src=src;script.type='module';script.onload=()=>resolve();script.onerror=()=>reject(newError(`Unable to load ${src}`));document.head.append(script);});scripts.set(src,promise);returnpromise;};exportdefaultcomponent$(()=>{constassetsReady=useSignal(false);constmessage=useSignal('Hello from the Qwik shell');useVisibleTask$(({cleanup})=>{consthandleMicroFrontendMessage=(event: Event)=>{const{source,message: nextMessage}=(eventasCustomEvent).detail;message.value=`${source}: ${nextMessage}`;};window.addEventListener('microfrontend:message',handleMicroFrontendMessage);Promise.all([loadScript(assetUrl('mfes/angular/polyfills.js')),loadScript(assetUrl('mfes/angular/main.js')),loadScript(assetUrl('mfes/react/react-microfrontend.js')),]).then(()=>{assetsReady.value=true;});cleanup(()=>{window.removeEventListener('microfrontend:message',handleMicroFrontendMessage);});});constupdateFromShell=$(()=>{message.value='Qwik updated the contract for every micro frontend';});return(<main><buttontype="button"onClick$={updateFromShell}>UpdatesharedmessagefromQwik</button>{assetsReady.value?(<><angular-microfrontendmessage={message.value}></angular-microfrontend><react-microfrontendmessage={message.value}></react-microfrontend></>):(<p>Loadingmicrofrontendbundles...</p>)}</main>);});
This keeps the integration simple:
Shell to micro frontend: pass data through custom element attributes
Micro frontend to shell: dispatch a microfrontend:message DOM event
Deployment paths: resolve bundles through import.meta.env.BASE_URL, so the same demo works locally and under /examples/qwik-angular-react-rust/
Repeated navigation: cache script-load promises and guard custom element registration, so the bundles are not registered twice
That is enough for a small demo and keeps each app loosely coupled.
npm install --prefix qwik-micro-frontend
npm install --prefix angular-app
npm install --prefix react-app
npm run dev
Visit http://localhost:5173. You should see the Qwik shell with Angular and React micro frontends on the same page. Click the shell button to push a new message to both micro frontends, then click a button inside Angular or React to send a message back to the shell.
The sample also includes a small Rust WASM helper. When wasm-pack is installed, the root build emits it into qwik-micro-frontend/public/mfes/rust-wasm, and the Qwik shell imports it dynamically:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
constimportBrowserModule=newFunction('src','return import(src)',)as<T>(src: string)=>Promise<T>;importBrowserModule<RustWasm>(assetUrl('mfes/rust-wasm/rust_wasm.js')).then(async(rust)=>{awaitrust.default();window.rustWasmApi=rust;rustStats.value=rust.analyze_message(message.value);}).catch(()=>{rustStats.value='Run `npm run build:rust` from the project root to enable Rust WASM.';});
The Rust side exposes two functions:
1
2
3
4
5
6
7
8
9
10
11
12
13
#[wasm_bindgen]pubfnanalyze_message(input: &str)-> String{letchars=input.chars().count();letwords=input.split_whitespace().count();letchecksum=input.bytes().fold(0u32,|acc,byte|acc.wrapping_add(byteasu32));format!("{chars} chars - {words} words - checksum {checksum}")}#[wasm_bindgen]pubfncount_primes(limit: u32)-> u32{// Prime sieve implementation used by the shell's benchmark button.
}
analyze_message updates whenever the shared message changes. count_primes powers the “Run prime sieve in Rust WASM” button, which gives the demo a small but real CPU-bound WebAssembly task. If wasm-pack is not installed, the Rust build is skipped by default so the JavaScript micro frontends still run.
In this example, we built a micro frontend architecture using Qwik as the shell, integrated Angular and React through Web Components, and included Rust WebAssembly for a small helper module. Instead of forcing every framework into one shared Redux store, the shell and micro frontends communicate through a small, explicit contract.
That keeps each micro frontend independently buildable and deployable while still giving users one cohesive page.