Project Structure for Microfrontends
Create three Next.js apps and wire them to a shared UI package.
Outcome
Create three Next.js applications (marketing, docs, dashboard) and a shared UI package with a Header component.
The Target Structure
acme-platform/
├── apps/
│ ├── marketing/ # Default app (/, /pricing, /about)
│ │ ├── app/
│ │ │ ├── layout.tsx
│ │ │ ├── page.tsx
│ │ │ ├── pricing/page.tsx
│ │ │ └── about/page.tsx
│ │ ├── next.config.ts
│ │ └── package.json
│ ├── docs/ # Child app (/docs/*)
│ │ ├── app/
│ │ │ ├── layout.tsx
│ │ │ └── docs/ # Routes match /docs paths
│ │ │ ├── page.tsx # /docs
│ │ │ └── [slug]/page.tsx # /docs/:slug
│ │ ├── next.config.ts
│ │ └── package.json
│ └── dashboard/ # Child app (/app/*, /settings/*)
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── app/ # Routes match /app paths
│ │ │ └── page.tsx # /app
│ │ └── settings/ # Routes match /settings paths
│ │ └── page.tsx # /settings
│ ├── next.config.ts
│ └── package.json
├── packages/
│ └── ui/ # Shared components
│ ├── src/
│ │ ├── header.tsx
│ │ └── index.ts
│ ├── package.json
│ └── tsconfig.json
├── turbo.json
└── package.json
Child apps must have their file structure match the routed paths. The docs app receives /docs/* requests, so it needs routes at app/docs/*. The dashboard receives /app/* and /settings/*, so it needs routes at app/app/* and app/settings/*.
Fast Track
- Create three Next.js apps in
apps/ - Create the shared UI package in
packages/ - Wire up dependencies and verify the Header renders in all apps
Hands-on Exercise 1.4
Build the complete project structure.
Part 1: Create the Marketing App
cd apps
pnpm create next-app@latest marketing --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"Update apps/marketing/package.json:
{
"name": "marketing",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3000",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^16.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@acme/ui": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0"
}
}Create apps/marketing/app/layout.tsx:
import type { Metadata } from "next";
import { Header } from "@acme/ui";
import "./globals.css";
export const metadata: Metadata = {
title: "Acme Platform",
description: "The everything app for your business",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-white flex flex-col">
<Header />
<main className="flex-1">{children}</main>
</body>
</html>
);
}Create apps/marketing/app/page.tsx:
export default function HomePage() {
return (
<div className="flex flex-col items-center justify-center min-h-[80vh] p-8">
<h1 className="text-4xl font-bold mb-4">Welcome to Acme Platform</h1>
<p className="text-gray-600 mb-8 text-center max-w-md">
The everything app for your business.
</p>
<nav className="flex gap-4">
<a href="/pricing" className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
Pricing
</a>
<a href="/docs" className="px-4 py-2 border border-gray-300 rounded hover:bg-gray-50">
Documentation
</a>
</nav>
</div>
);
}Create apps/marketing/app/pricing/page.tsx:
export default function PricingPage() {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">Pricing</h1>
<p className="text-gray-600">Simple, transparent pricing for your business.</p>
</div>
);
}Part 2: Create the Docs App
pnpm create next-app@latest docs --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"Update apps/docs/package.json:
{
"name": "docs",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3001",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^16.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@acme/ui": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0"
}
}Create the docs route structure. First, create the directory:
mkdir -p apps/docs/app/docsCreate apps/docs/app/layout.tsx:
import type { Metadata } from "next";
import { Header } from "@acme/ui";
import "./globals.css";
export const metadata: Metadata = {
title: "Acme Docs",
description: "Documentation for Acme Platform",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-white flex flex-col">
<Header />
<main className="flex-1">{children}</main>
</body>
</html>
);
}Create apps/docs/app/docs/page.tsx:
export default function DocsHomePage() {
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-2">Documentation</h1>
<p className="text-gray-600 mb-8">
Everything you need to build with Acme Platform.
</p>
<div className="grid gap-4">
<a href="/docs/getting-started" className="block p-4 border rounded-lg hover:border-blue-500">
<h2 className="font-semibold">Getting Started</h2>
<p className="text-gray-600 text-sm">Learn the basics</p>
</a>
<a href="/docs/api" className="block p-4 border rounded-lg hover:border-blue-500">
<h2 className="font-semibold">API Reference</h2>
<p className="text-gray-600 text-sm">Complete API documentation</p>
</a>
</div>
</div>
);
}Part 3: Create the Dashboard App
pnpm create next-app@latest dashboard --typescript --tailwind --eslint --app --src-dir=false --import-alias="@/*"Update apps/dashboard/package.json:
{
"name": "dashboard",
"version": "0.0.0",
"private": true,
"scripts": {
"dev": "next dev --port 3002",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "^16.1.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"@acme/ui": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.10.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"typescript": "^5.7.0",
"tailwindcss": "^3.4.0",
"postcss": "^8.4.0",
"autoprefixer": "^10.4.0"
}
}Create the dashboard route structure. First, create the directories:
mkdir -p apps/dashboard/app/app apps/dashboard/app/settingsCreate apps/dashboard/app/layout.tsx:
import type { Metadata } from "next";
import { Header } from "@acme/ui";
import "./globals.css";
export const metadata: Metadata = {
title: "Acme Dashboard",
description: "Manage your Acme Platform",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="min-h-screen bg-gray-50">
<Header />
<main className="p-8">{children}</main>
</body>
</html>
);
}Create apps/dashboard/app/app/page.tsx:
export default function DashboardPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-6">Dashboard Overview</h1>
<div className="grid md:grid-cols-3 gap-4">
<div className="bg-white p-6 rounded-lg border">
<h2 className="text-gray-500 text-sm mb-1">Total Projects</h2>
<p className="text-3xl font-bold">12</p>
</div>
<div className="bg-white p-6 rounded-lg border">
<h2 className="text-gray-500 text-sm mb-1">Active Deployments</h2>
<p className="text-3xl font-bold">8</p>
</div>
</div>
</div>
);
}Create apps/dashboard/app/settings/page.tsx:
export default function SettingsPage() {
return (
<div>
<h1 className="text-2xl font-bold mb-6">Settings</h1>
<div className="bg-white rounded-lg border p-6 max-w-2xl">
<h2 className="font-semibold mb-4">Account Settings</h2>
<p className="text-gray-600">Manage your account settings here.</p>
</div>
</div>
);
}Part 4: Create the Shared UI Package
Create the package directory:
cd ../packages
mkdir -p ui/srcCreate packages/ui/package.json:
{
"name": "@acme/ui",
"version": "0.0.0",
"private": true,
"exports": {
".": {
"types": "./src/index.ts",
"default": "./src/index.ts"
}
},
"scripts": {
"lint": "eslint ."
},
"peerDependencies": {
"react": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"typescript": "^5.7.0"
}
}Create packages/ui/tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "ES2020"],
"jsx": "react-jsx",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}Create packages/ui/src/header.tsx:
export function Header() {
return (
<header className="h-16 border-b bg-white px-6 flex items-center justify-between">
<div className="flex items-center gap-8">
<a href="/" className="font-bold text-xl">
Acme
</a>
<nav className="hidden md:flex items-center gap-6">
<a href="/" className="text-gray-600 hover:text-gray-900">
Home
</a>
<a href="/docs" className="text-gray-600 hover:text-gray-900">
Docs
</a>
<a href="/pricing" className="text-gray-600 hover:text-gray-900">
Pricing
</a>
</nav>
</div>
<div className="flex items-center gap-4">
<a
href="/app"
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Dashboard
</a>
</div>
</header>
);
}Create packages/ui/src/index.ts:
export { Header } from "./header";Part 5: Install All Dependencies
From the root:
cd ../..
pnpm installTry It
Run all three apps simultaneously:
pnpm devYou should see Turborepo output showing all three apps starting:
• Packages in scope: @acme/ui, dashboard, docs, marketing
• Running dev in 3 packages
marketing:dev: ready - started server on 0.0.0.0:3000
docs:dev: ready - started server on 0.0.0.0:3001
dashboard:dev: ready - started server on 0.0.0.0:3002
Visit each app at its correct route:
- http://localhost:3000 - Marketing (Home)
- http://localhost:3001/docs - Docs
- http://localhost:3002/app - Dashboard
- http://localhost:3002/settings - Settings
The same Header component should render in all three, with the current app highlighted.
Commit
git add -A
git commit -m "feat: add marketing, docs, and dashboard apps with shared UI"Done-When
- Three Next.js apps created in
apps/ - Shared UI package in
packages/ui/ - Header component renders in all three apps
pnpm devruns all apps simultaneously- Navigation links work (but route to wrong ports for now)
Navigation links won't work yet. Clicking "Docs" from localhost:3000 tries to load localhost:3000/docs, which doesn't exist. The apps are still separate. Section 2 stitches them together.
What's Next
Three apps, one shared package, but no routing between them. Section 2 adds microfrontends.json and wires it all together.
Was this helpful?