Turborepo is the standard for JavaScript/TypeScript monorepos in 2026 โ build caching alone cuts CI time by 70-90% on large codebases. This guide sets up a production-ready monorepo with two Next.js apps and shared packages from scratch.
๐ Table of Contents
- What We're Building
- Step 1: Initialize with Turborepo
- Step 2: Root package.json
- Step 3: pnpm Workspace Configuration
- Step 4: turbo.json โ Pipeline Configuration
- Step 5: Shared TypeScript Config Package
- Step 6: Shared UI Package
- Step 7: Next.js App Using Shared Package
- Step 8: Running the Monorepo
- Step 9: Remote Caching for CI
- Frequently Asked Questions
- Conclusion
๐ Key Takeaway
Turborepo is the standard for JavaScript/TypeScript monorepos in 2026 โ build caching alone cuts CI time by 70-90% on large codebases. This guide sets up a production-ready monorepo with two Next.js apps and shared packages from scratch.
What We’re Building
my-monorepo/
โโโ apps/
โ โโโ web/ # Next.js marketing site
โ โโโ docs/ # Next.js documentation site
โโโ packages/
โ โโโ ui/ # Shared React components
โ โโโ utils/ # Shared utilities
โ โโโ tsconfig/ # Shared TypeScript configs
โโโ turbo.json # Turborepo configuration
โโโ package.json # Workspace root
Step 1: Initialize with Turborepo
# Create from official template
npx create-turbo@latest my-monorepo
cd my-monorepo
# Or manual setup
mkdir my-monorepo && cd my-monorepo
pnpm init
# pnpm is recommended for monorepos โ faster, better disk usage
Step 2: Root package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test",
"format": "prettier --write "**/*.{ts,tsx,md}""
},
"devDependencies": {
"turbo": "^2.0.0",
"prettier": "^3.0.0",
"typescript": "^5.0.0"
},
"packageManager": "pnpm@9.0.0"
}
Step 3: pnpm Workspace Configuration
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
Step 4: turbo.json โ Pipeline Configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // build dependencies first
"outputs": [".next/**", "dist/**", "!.next/cache/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_*"] // cache key includes these env vars
},
"dev": {
"cache": false, // never cache dev servers
"persistent": true // keep running (don't wait for exit)
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"],
"outputs": ["coverage/**"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
Step 5: Shared TypeScript Config Package
mkdir -p packages/tsconfig
cat > packages/tsconfig/package.json << 'EOF'
{
"name": "@mymonorepo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["*.json"]
}
EOF
// packages/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Default",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "ESNext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"allowJs": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"noEmit": true
}
}
// packages/tsconfig/nextjs.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"display": "Next.js",
"extends": "./base.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"jsx": "preserve",
"module": "ESNext"
}
}
Step 6: Shared UI Package
mkdir -p packages/ui/src
cat > packages/ui/package.json << 'EOF'
{
"name": "@mymonorepo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": "./src/button.tsx",
"./card": "./src/card.tsx"
},
"scripts": {
"lint": "eslint src/",
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@mymonorepo/tsconfig": "workspace:*",
"typescript": "^5.0.0"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
}
EOF
// packages/ui/src/button.tsx
import { type ButtonHTMLAttributes } from "react";
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: "primary" | "secondary";
}
export function Button({ variant = "primary", children, ...props }: ButtonProps) {
return (
<button
className={variant === "primary" ? "btn-primary" : "btn-secondary"}
{...props}
>
{children}
</button>
);
}
Step 7: Next.js App Using Shared Package
# apps/web/package.json
{
"name": "web",
"dependencies": {
"@mymonorepo/ui": "workspace:*",
"@mymonorepo/utils": "workspace:*",
"next": "^15.0.0",
"react": "^19.0.0"
},
"devDependencies": {
"@mymonorepo/tsconfig": "workspace:*"
}
}
// apps/web/tsconfig.json
{
"extends": "@mymonorepo/tsconfig/nextjs.json",
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}
// apps/web/app/page.tsx
import { Button } from "@mymonorepo/ui/button"; // shared component
export default function HomePage() {
return (
<main>
<h1>Welcome</h1>
<Button variant="primary">Get Started</Button>
</main>
);
}
Step 8: Running the Monorepo
# Install all dependencies
pnpm install
# Run all dev servers in parallel
pnpm dev
# Build all apps and packages (with caching)
pnpm build
# Build only one app
pnpm turbo build --filter=web
# Build only changed packages (incremental)
pnpm turbo build --filter="[HEAD^1]"
Step 9: Remote Caching for CI
# Turbo remote cache with Vercel (free tier available)
npx turbo login
npx turbo link
# Now CI can use remote cache โ artifacts shared across team
# .github/workflows/ci.yml
- name: Build
run: pnpm turbo build
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Cache hit rates in practice: On a team of 5 developers, cache hit rates of 70-80% are common on CI. A build that takes 8 minutes on first run often takes under 1 minute with cache hits โ the ROI is significant for any team running CI multiple times per day.
Frequently Asked Questions
Q: pnpm vs npm vs yarn for monorepos?
A: pnpm is the clear choice for monorepos in 2026 โ symlinked node_modules use 60-80% less disk space vs npm, workspace protocol (workspace:*) is more reliable, and installation is significantly faster. Most new Turborepo projects use pnpm.
Q: When should I use a monorepo?
A: When you have 2+ related projects sharing code (types, components, utilities). Single project: don’t bother. 2-3 closely related apps: monorepo pays off quickly. The break-even point is roughly “you’re tired of copying utility functions between repos.”
Q: How do I add a new package?
A: mkdir packages/my-package && cd packages/my-package && pnpm init, add it to the workspace, then pnpm install. The package is immediately available via workspace:* in other packages.
Q: Is Turborepo better than Nx?
A: Turborepo is simpler to set up and configure. Nx has more built-in generators and a richer plugin ecosystem. Turborepo for smaller teams who want simplicity; Nx for large organizations with complex build graphs. Both are production-proven.
Q: How do I deploy individual apps from a monorepo?
A: Vercel and Netlify have native monorepo support โ configure “Root Directory” to the app subdirectory. Alternatively, use Docker with COPY . . at monorepo root and turbo build --filter=app-name.
Conclusion
Turborepo transforms multi-app JavaScript development by eliminating redundant builds and sharing code cleanly. The setup โ pnpm workspaces, shared tsconfig package, shared UI package, turbo.json pipeline, remote caching โ takes about 2 hours to configure but pays back in every subsequent build. Start with the create-turbo template for the fastest path, or follow this guide for a fully customized setup that matches your actual project structure.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment