Vercel Logo

Shared Packages

Some things should stay unified across apps: headers, footers, design systems, auth logic. Shared packages handle this.

Outcome

Build a complete shared UI package with Header and Footer components, plus a utils package for shared authentication helpers.

Why Shared Packages

Without shared packages:

apps/marketing/components/Header.tsx  ← Copy 1
apps/docs/components/Header.tsx       ← Copy 2 (might drift)
apps/dashboard/components/Header.tsx  ← Copy 3 (definitely drifts)

With shared packages:

packages/ui/src/header.tsx            ← Single source of truth
apps/*/app/layout.tsx                 ← All import from @acme/ui

When you update the Header, all apps get the update on their next build.

Fast Track

  1. Expand the UI package with Header and Footer
  2. Create a utils package for auth helpers
  3. Use shared components in all app layouts

Hands-on Exercise 2.4

Build complete shared packages for Acme Platform.

Part 1: Expand the UI Package

Update packages/ui/src/header.tsx with improved styling and navigation:

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/footer.tsx:

packages/ui/src/footer.tsx
export function Footer() {
  return (
    <footer className="border-t bg-gray-50 px-6 py-8">
      <div className="max-w-6xl mx-auto flex flex-col md:flex-row justify-between gap-8">
        <div>
          <p className="font-bold text-lg">Acme Platform</p>
          <p className="text-gray-600 text-sm mt-1">
            The everything app for your business.
          </p>
        </div>
        <div className="flex gap-12">
          <div>
            <p className="font-semibold mb-2">Product</p>
            <nav className="flex flex-col gap-1 text-sm text-gray-600">
              <a href="/pricing" className="hover:text-gray-900">Pricing</a>
              <a href="/docs" className="hover:text-gray-900">Documentation</a>
            </nav>
          </div>
          <div>
            <p className="font-semibold mb-2">Company</p>
            <nav className="flex flex-col gap-1 text-sm text-gray-600">
              <a href="/about" className="hover:text-gray-900">About</a>
              <a href="/contact" className="hover:text-gray-900">Contact</a>
            </nav>
          </div>
        </div>
      </div>
      <div className="max-w-6xl mx-auto mt-8 pt-4 border-t text-center text-sm text-gray-500">
        &copy; {new Date().getFullYear()} Acme Inc. All rights reserved.
      </div>
    </footer>
  );
}

Update packages/ui/src/index.ts:

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

Part 2: Create the Utils Package

Create a shared utilities package for authentication and other helpers.

Create packages/utils/package.json:

packages/utils/package.json
{
  "name": "@acme/utils",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "default": "./src/index.ts"
    }
  },
  "devDependencies": {
    "typescript": "^5.7.0"
  }
}

Create packages/utils/tsconfig.json:

packages/utils/tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src"]
}

Create packages/utils/src/auth.ts:

packages/utils/src/auth.ts
import type { NextRequest } from "next/server";
 
export interface AuthToken {
  userId: string;
  email: string;
  exp: number;
}
 
export function getAuthToken(request: NextRequest): string | undefined {
  return request.cookies.get("auth-token")?.value;
}
 
export function isAuthenticated(request: NextRequest): boolean {
  const token = getAuthToken(request);
  if (!token) return false;
  // In a real app, verify the JWT here
  return true;
}
 
export function decodeToken(token: string): AuthToken | null {
  try {
    // In a real app, use a proper JWT library
    const payload = JSON.parse(atob(token.split(".")[1]));
    return payload as AuthToken;
  } catch {
    return null;
  }
}

Create packages/utils/src/index.ts:

packages/utils/src/index.ts
export { getAuthToken, isAuthenticated, decodeToken } from "./auth";
export type { AuthToken } from "./auth";

Part 3: Add UI Package as Dependency

Add @acme/ui to all three apps. Update each package.json:

apps/marketing/package.json:

apps/marketing/package.json
{
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@vercel/microfrontends": "^2.3.0",
    "next": "16.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

apps/docs/package.json:

apps/docs/package.json
{
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@vercel/microfrontends": "^2.3.0",
    "next": "16.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

apps/dashboard/package.json:

apps/dashboard/package.json
{
  "dependencies": {
    "@acme/ui": "workspace:*",
    "@vercel/microfrontends": "^2.3.0",
    "next": "16.1.1",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Run pnpm install to link the workspace dependencies.

Part 4: Configure Tailwind to Scan Shared Packages

Tailwind only scans files in the content array. Without this step, shared component styles won't work.

Update tailwind.config.ts in all three apps:

apps/marketing/tailwind.config.ts
import type { Config } from "tailwindcss";
 
const config: Config = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx,mdx}",
    "./components/**/*.{js,ts,jsx,tsx,mdx}",
    "../../packages/ui/src/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
 
export default config;

Apply the same change to apps/docs/tailwind.config.ts and apps/dashboard/tailwind.config.ts.

If shared component styles don't appear, you probably forgot to add the packages path to Tailwind's content array.

Update apps/marketing/app/layout.tsx:

apps/marketing/app/layout.tsx
import type { Metadata } from "next";
import { Header, Footer } 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>
        <Footer />
      </body>
    </html>
  );
}

Update apps/docs/app/layout.tsx:

apps/docs/app/layout.tsx
import type { Metadata } from "next";
import { Header, Footer } 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>
        <Footer />
      </body>
    </html>
  );
}

Update 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>
  );
}

Part 6: Install Dependencies and Test

pnpm install
pnpm dev

Try It

  1. Visit localhost:3024
  2. Navigate between apps - the Header and Footer should be consistent
  3. Check that Tailwind styles are applied (proper spacing, colors, hover states)

Commit

git add -A
git commit -m "feat: add shared Header and Footer components"

Done-When

  • Header component renders in all three apps
  • Footer component renders in marketing and docs apps
  • Tailwind styles are applied correctly (not unstyled)
  • Consistent look and feel across all apps

Package Versioning Strategy

In this monorepo setup, packages use workspace:* for dependencies:

"@acme/ui": "workspace:*"

This means "use the version in this workspace." All apps always use the same version.

For published packages (external to monorepo), you'd want explicit version pinning and a versioning strategy. But for internal packages in a monorepo, workspace:* keeps everything in sync.

What's Next

Deploy to Vercel and see routing work in production with fallback behavior.