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 border-gray-200 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 border-gray-200 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 border-gray-200 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": "^6"
  }
}

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",
    "next": "^16",
    "react": "^19",
    "react-dom": "^19"
  }
}

apps/docs/package.json:

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

apps/dashboard/package.json:

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

Run pnpm install to link the workspace dependencies.

Part 4: Configure Tailwind to Scan Shared Packages

If you've used Tailwind 3, you're looking for a content array in tailwind.config.ts. Don't bother: create-next-app sets up Tailwind 4, which has no config file and finds class names on its own. It only looks inside the app's own folder, though, so the components in packages/ui never get scanned. Without this step, the Header renders with no styles.

Tell Tailwind about the shared package with @source in each app's globals.css. The path is relative to the CSS file:

apps/marketing/app/globals.css
@import "tailwindcss";
@source "../../../packages/ui/src";

Keep the rest of the generated file below those two lines. Apply the same change to apps/docs/app/globals.css and apps/dashboard/app/globals.css.

If shared component styles don't appear, check the @source path first. One ../ too few is the usual culprit.

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.

Was this helpful?

supported.