A proof of concept demonstrating a micro frontend architecture using Turborepo as the monorepo orchestrator and Next.js Multi-Zones as the shell pattern. Five independent micro frontends — each built with a different framework — are composed under a single entry point. A shared Web Component (<app-nav>) provides consistent navigation across all zones with zero build-time coupling.
┌─────────────────────┐
│ Browser Request │
│ http://localhost:3000│
└──────────┬───────────┘
│
┌──────────▼───────────┐
│ Shell │
│ (Next.js :3000) │
│ │
│ - Rewrite hub │
│ - Home page │
│ - NO nav rendering │
└──────────┬───────────┘
│
┌─────────────────────┼─────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ beforeFiles │ │ fallback │ │ fallback │
│ rewrites │ │ rewrites │ │ rewrites │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │
┌─────────▼──────────┐ │ │
│ Next.js App │ │ │
│ /nextapp → :3001 │ │ │
│ (Multi-Zone) │ │ │
└────────────────────┘ │ │
┌────────┴────────────────────────┐
│ Vite / Angular │
│ Dev Servers │
├────────────────────────────────┤
│ React /reactapp → :3002 │
│ Svelte /svelteapp → :3003 │
│ Vue /vueapp → :3004 │
│ Angular /angularapp→ :3005 │
└────────────────────────────────┘
┌──────────────────────────────────────────────────────┐
│ Nav Component (:3006) │
│ <app-nav> Web Component loaded by ALL zones │
│ via <script src="http://localhost:3006/..."> │
│ │
│ Zero build-time coupling — each zone loads │
│ the nav at runtime, no npm dependency needed │
└──────────────────────────────────────────────────────┘
The shell acts as a rewrite hub in development. It does not render navigation — that responsibility belongs to each zone via the shared <app-nav> Web Component. All user traffic enters through localhost:3000, and the shell forwards requests to the appropriate micro frontend's dev server.
Navigation between micro frontends triggers a full-page browser navigation (vertical splitting). Each app owns its own JavaScript bundle, framework runtime, and application state.
The <app-nav> custom element is a framework-agnostic Web Component that provides consistent navigation across all zones. It is built with vanilla TypeScript and Vite, uses Shadow DOM for style isolation, and is loaded by each zone via a <script> tag at runtime.
nav-component app (apps/nav-component/) builds a single JavaScript file (nav.js)<script type="module" src="http://localhost:3006/src/nav.ts"> in its HTML<app-nav> with its own links and app-name attributescustomElements.define("app-nav", AppNav)<script type="module" src="http://localhost:3006/src/nav.ts"></script>
<app-nav
app-name="Micro Frontend PoC"
links='[{"label":"Home","href":"/"},{"label":"Next.js","href":"/nextapp"}]'
></app-nav>
| Attribute | Type | Description |
|---|---|---|
app-name |
string | Title displayed on the left side of the nav bar |
links |
JSON | Stringified array of { label, href } objects |
Each zone defines its own links — this means adding a link to a zone's nav is a change inside that zone only, not the shared component.
micro-frontend/
├── apps/
│ ├── shell/ # Next.js rewrite hub (port 3000)
│ │ ├── src/app/
│ │ │ ├── layout.tsx # Loads <app-nav> via <Script>, no inline nav
│ │ │ └── page.tsx # Home page with framework cards
│ │ └── next.config.ts # Rewrite rules for all micro frontends
│ │
│ ├── nav-component/ # Web Component nav (port 3006)
│ │ ├── src/
│ │ │ └── nav.ts # <app-nav> custom element (Shadow DOM)
│ │ ├── index.html # Dev harness for testing in isolation
│ │ └── vite.config.ts # Lib mode build → nav.js
│ │
│ ├── next-app/ # Next.js micro frontend (port 3001)
│ │ ├── src/app/
│ │ │ ├── layout.tsx # Loads <app-nav> via <Script>
│ │ │ └── page.tsx
│ │ └── next.config.ts # basePath + assetPrefix config
│ │
│ ├── react-app/ # React + Vite micro frontend (port 3002)
│ │ ├── src/
│ │ │ ├── main.tsx
│ │ │ └── App.tsx
│ │ ├── index.html # Loads <app-nav> in HTML
│ │ └── vite.config.ts
│ │
│ ├── svelte-app/ # SvelteKit micro frontend (port 3003)
│ │ ├── src/
│ │ │ ├── app.html # Loads <app-nav> in HTML
│ │ │ └── routes/
│ │ │ └── +page.svelte
│ │ └── svelte.config.js
│ │
│ ├── vue-app/ # Vue + Vite micro frontend (port 3004)
│ │ ├── src/
│ │ │ ├── main.ts
│ │ │ └── App.vue
│ │ ├── index.html # Loads <app-nav> in HTML
│ │ └── vite.config.ts
│ │
│ └── angular-app/ # Angular micro frontend (port 3005)
│ ├── src/
│ │ ├── index.html # Loads <app-nav> in HTML
│ │ ├── main.ts
│ │ └── app/
│ │ ├── app.ts
│ │ └── app.html
│ └── angular.json
│
├── packages/
│ └── shared/ # Shared constants and types
│ └── src/
│ └── index.ts # App registry, colors, types
│
├── turbo.json # Turborepo task configuration
├── pnpm-workspace.yaml # pnpm workspace definition
├── package.json # Root scripts
└── README.md
| Tool | Minimum Version | Check Command |
|---|---|---|
| Node.js | 18.x or later | node -v |
| pnpm | 9.x or later | pnpm -v |
If you don't have pnpm installed:
npm install -g pnpm
Or use Corepack (bundled with Node.js 16.9+):
corepack enable
corepack prepare pnpm@latest --activate
# 1. Clone the repository
git clone https://github.com/gAlexander77/micro-frontend.git
cd micro-frontend
# 2. Install all dependencies
pnpm install
# 3. Start the entire application (all 7 dev servers)
pnpm dev
Open http://localhost:3000 in your browser. You will see the shell home page with the Web Component nav bar and cards linking to each micro frontend.
The single pnpm dev command starts all seven dev servers in parallel via Turborepo:
:3000), Next.js App (:3001), React (:3002), Svelte (:3003), Vue (:3004), Angular (:3005), Nav Component (:3006)All scripts are run from the repository root.
| Command | Description |
|---|---|
pnpm dev |
Start all 7 dev servers in parallel (shell + 5 zones + nav component) |
pnpm build |
Build all apps for production |
pnpm lint |
Run linters across all apps |
pnpm clean |
Remove build artifacts from all apps |
To start only one app (useful for focused development), use pnpm's filter:
# Start only the shell
pnpm --filter @micro-frontend/shell dev
# Start only the React micro frontend
pnpm --filter @micro-frontend/react-app dev
# Start the shell, nav component, and next-app together
pnpm --filter @micro-frontend/shell --filter @micro-frontend/nav-component --filter @micro-frontend/next-app dev
Note: The nav component must be running for any zone's navigation to render. When developing a single zone, start both the zone and the nav component.
The shell is the rewrite hub for the entire application. It provides:
next.config.ts that proxy requests to zone dev servers<app-nav> Web Component — it does not render navigation itselfThe shell is intentionally minimal. Its only routing job is forwarding requests. Navigation is owned by the Web Component.
A vanilla TypeScript + Vite app that builds the <app-nav> custom element. This is the single source of truth for navigation chrome across all zones.
app-name and links attributesvite build outputs a single nav.js file)/src/nav.tsDev harness: http://localhost:3006
Config file: apps/nav-component/vite.config.ts
export default defineConfig({
build: {
lib: {
entry: "src/nav.ts",
formats: ["es"],
fileName: () => "nav.js",
},
},
server: { port: 3006, strictPort: true, cors: true },
});
A standalone Next.js application integrated via the Multi-Zones pattern.
basePath: "/nextapp" — all routes live under /nextappassetPrefix: "/nextapp" — static assets load from /nextapp/_next/...<app-nav> via next/script in its own layout.tsxURL via shell: http://localhost:3000/nextapp Direct URL: http://localhost:3001/nextapp
A client-side React application built with Vite.
base: "/reactapp/" in Vite config ensures correct asset resolution<app-nav> loaded in index.html before the React rootURL via shell: http://localhost:3000/reactapp Direct URL: http://localhost:3002/reactapp/
A SvelteKit application using the auto adapter.
paths.base: "/svelteapp" in SvelteKit config prefixes all routes and assets<app-nav> loaded in app.html (SvelteKit's HTML template)URL via shell: http://localhost:3000/svelteapp Direct URL: http://localhost:3003/svelteapp
A Vue 3 application built with Vite.
base: "/vueapp/" in Vite config ensures correct asset resolution<app-nav> loaded in index.html before the Vue mount pointURL via shell: http://localhost:3000/vueapp Direct URL: http://localhost:3004/vueapp/
An Angular application (v21) using the Angular CLI.
baseHref: "/angularapp/" in angular.json prefixes all asset paths<app-nav> loaded in src/index.html before <app-root>URL via shell: http://localhost:3000/angularapp Direct URL: http://localhost:3005/angularapp
The @micro-frontend/shared package (packages/shared/) provides shared constants consumed by any app in the monorepo.
| Export | Type | Description |
|---|---|---|
APP_NAME |
string |
Application title: "Micro Frontend PoC" |
APPS |
readonly AppInfo[] |
Registry of all micro frontends with name, path, and framework |
COLORS |
Record<string, string> |
Framework brand colors for UI styling |
AppInfo |
type |
TypeScript type for an app entry |
This package has no build step — it exports raw TypeScript files. It is used by the shell's home page for the card grid display. Navigation links are not sourced from this package — each zone defines its own links attribute on the <app-nav> element.
The Next.js app (next-app) is integrated using the Multi-Zones pattern:
basePath: "/nextapp" and assetPrefix: "/nextapp"/nextapp/** requests to the child app's server (localhost:3001)The rewrite is placed in beforeFiles so it takes priority over the shell's own filesystem routes.
Non-Next.js apps (React, Svelte, Vue, Angular) cannot use Multi-Zones directly. The shell uses rewrites() in the fallback section to proxy requests:
Browser → localhost:3000/reactapp → rewrite → localhost:3002/reactapp/
Browser → localhost:3000/svelteapp → rewrite → localhost:3003/svelteapp
Browser → localhost:3000/vueapp → rewrite → localhost:3004/vueapp/
Browser → localhost:3000/angularapp → rewrite → localhost:3005/angularapp/
Every micro frontend must know its own base path so it generates correct asset URLs.
| Framework | Config Property | Value |
|---|---|---|
| Next.js | basePath + assetPrefix |
"/nextapp" |
| React/Vite | base |
"/reactapp/" |
| SvelteKit | paths.base |
"/svelteapp" |
| Vue/Vite | base |
"/vueapp/" |
| Angular | baseHref |
"/angularapp/" |
If the nav is a shared npm package (e.g., @yourco/config-layout) compiled into each zone's bundle:
The nav is built as a standalone Web Component deployed to its own URL:
nav.js) — zero zone rebuilds| Aspect | Shared npm package | Web Component |
|---|---|---|
| Nav change deploys | All zones must rebuild | Only nav-component redeploys |
| Build-time coupling | Yes (compiled into each bundle) | None (loaded at runtime) |
| Framework requirement | Must match host framework | Framework-agnostic (custom element) |
| Style isolation | CSS-in-JS or careful scoping | Shadow DOM (automatic) |
| Independent deployment | Broken for nav changes | Fully preserved |
Every change touches exactly one app:
| What changed | Who redeploys | Zone rebuilds? |
|---|---|---|
| Nav styles / structure | nav-component only |
Zero |
| Course Planner adds route | course-planner only |
Zero |
| React app bug fix | react-app only |
Zero |
| Shell rewrite rules | shell only |
Zero |
| Shared constants | Affected consumers | Only those |
To add a new micro frontend (e.g., a Remix app on port 3007):
mkdir apps/remix-app
cd apps/remix-app
# Initialize your framework of choice
Ensure the package.json has a dev script:
{
"name": "@micro-frontend/remix-app",
"scripts": { "dev": "remix dev --port 3007" }
}
Configure the framework to serve under /remixapp.
In the app's HTML entry point, add:
<script type="module" src="http://localhost:3006/src/nav.ts"></script>
<app-nav
app-name="Micro Frontend PoC"
links='[{"label":"Home","href":"/"},{"label":"Remix App","href":"/remixapp"}]'
></app-nav>
In apps/shell/next.config.ts, add entries to the fallback array:
{ source: "/remixapp", destination: "http://localhost:3007/remixapp" },
{ source: "/remixapp/:path*", destination: "http://localhost:3007/remixapp/:path*" },
pnpm install
pnpm dev
Turborepo automatically picks up the new app since pnpm-workspace.yaml includes apps/*.
In production, the nav component should be built (vite build outputs dist/nav.js) and deployed to a CDN or its own static hosting:
https://nav.yourcompany.com/nav.js
Each zone's HTML would reference the production URL instead of localhost:3006:
<script type="module" src="https://nav.yourcompany.com/nav.js"></script>
Replace the Next.js rewrite-based proxying with a dedicated routing layer:
location blocks per path prefixvercel.json rewrites with each app deployed independentlyFor AWS Amplify deployments from a monorepo:
AMPLIFY_MONOREPO_APP_ROOT pointing to its directoryappRoot changeappRoot) require incoming webhooks + GitHub Actions:# .github/workflows/deploy.yml
- name: Check if shared package changed
run: |
CHANGED=$(git diff --name-only HEAD^1 HEAD)
if echo "$CHANGED" | grep -q "packages/"; then
echo "changed=true" >> $GITHUB_OUTPUT
fi
- name: Trigger zone rebuild
if: steps.check.outputs.changed == 'true'
run: curl -X POST "${{ secrets.AMPLIFY_WEBHOOK_ZONE }}"
Each micro frontend is deployable independently:
pnpm --filter @micro-frontend/react-app build
This PoC does not implement cross-app state sharing. For production, consider:
window.postMessage, CustomEvent) for inter-app communicationIf you see Error: Port XXXX is already in use, kill the process occupying that port:
# macOS / Linux
lsof -ti:3000 | xargs kill -9
# Windows
netstat -ano | findstr :3000
taskkill /PID <PID> /F
Or kill all dev servers at once:
# macOS / Linux
for port in 3000 3001 3002 3003 3004 3005 3006; do
lsof -ti:$port | xargs kill -9 2>/dev/null
done
# Windows (PowerShell)
3000..3006 | ForEach-Object {
$connections = Get-NetTCPConnection -LocalPort $_ -ErrorAction SilentlyContinue
$connections | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
}
The <app-nav> Web Component requires the nav-component dev server to be running on port 3006. If the nav bar is missing:
curl http://localhost:3006pnpm --filter @micro-frontend/nav-component devWhen accessing micro frontends through the shell proxy (localhost:3000/reactapp), HMR WebSocket connections may not be proxied. Two options:
localhost:3002/reactapp/)server: {
port: 3002,
strictPort: true,
hmr: { port: 3002 },
}
SvelteKit may redirect from /svelteapp to /svelteapp/ (with trailing slash). The rewrite rules handle both patterns. Check the browser's Network tab for redirect chains.
Angular's build system compiles the full application on startup. The first load takes longer than Vite-based apps. Subsequent reloads are fast due to incremental compilation.
Ensure you're using a compatible pnpm version. The project specifies "packageManager": "[email protected]" in the root package.json. Use Corepack to match:
corepack enable
corepack install
| Layer | Technology |
|---|---|
| Monorepo | Turborepo + pnpm workspaces |
| Shell | Next.js 15 (App Router) |
| Nav Component | Vanilla TypeScript + Vite (Web Component) |
| Micro Frontend 1 | Next.js 15 (Multi-Zones) |
| Micro Frontend 2 | React 19 + Vite 6 |
| Micro Frontend 3 | SvelteKit 2 + Svelte 5 |
| Micro Frontend 4 | Vue 3 + Vite 6 |
| Micro Frontend 5 | Angular 21 |
| Shared Package | TypeScript (no build step) |