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
- Expand the UI package with Header and Footer
- Create a utils package for auth helpers
- 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:
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:
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">
© {new Date().getFullYear()} Acme Inc. All rights reserved.
</div>
</footer>
);
}Update 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:
{
"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:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true
},
"include": ["src"]
}Create 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:
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:
{
"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:
{
"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:
{
"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:
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.
Part 5: Update Layouts to Use Header and Footer
Update 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:
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:
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 devTry It
- Visit
localhost:3024 - Navigate between apps - the Header and Footer should be consistent
- 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?