Vercel Logo

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

  1. Create three Next.js apps in apps/
  2. Create the shared UI package in packages/
  3. 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:

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:

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:

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:

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:

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/docs

Create apps/docs/app/layout.tsx:

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:

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:

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/settings

Create apps/dashboard/app/layout.tsx:

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:

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:

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/src

Create packages/ui/package.json:

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:

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:

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:

packages/ui/src/index.ts
export { Header } from "./header";

Part 5: Install All Dependencies

From the root:

cd ../..
pnpm install

Try It

Run all three apps simultaneously:

pnpm dev

You 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:

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 dev runs 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.