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 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:
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">
© {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": "^6"
}
}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",
"next": "^16",
"react": "^19",
"react-dom": "^19"
}
}apps/docs/package.json:
{
"dependencies": {
"@acme/ui": "workspace:*",
"@vercel/microfrontends": "^2",
"next": "^16",
"react": "^19",
"react-dom": "^19"
}
}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:
@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.
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?