Monorepos get slow as they grow — rebuilding everything on every change defeats the purpose. Turborepo fixes this with task pipelines and content-addressed caching: only rebuild what changed, cache outputs locally and remotely, and share code between apps without the complexity of publishing internal packages.
Claude Code generates Turborepo configurations, pipeline definitions, and the workspace structure that makes monorepos fast.
Initial Setup
Convert our apps/api and apps/web monorepo to Turborepo.
We want: lint, build, and test tasks with correct dependency ordering.
Build web only after api types are built.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"], // "^" means: run build in dependencies first
"outputs": [
"dist/**",
".next/**",
"!.next/cache/**" // Exclude Next.js build cache from Turborepo cache
]
},
"test": {
"dependsOn": ["^build"], // Tests can run only after deps are built
"outputs": ["coverage/**"],
"cache": true
},
"lint": {
"outputs": [],
"cache": true // Lint results are cached per file hash
},
"dev": {
"cache": false, // Never cache dev servers
"persistent": true // Long-running process
},
"type-check": {
"dependsOn": ["^build"],
"outputs": ["*.tsbuildinfo"]
},
"generate": {
"cache": false // Code generation should always run
}
}
}
// package.json (root)
{
"name": "my-monorepo",
"private": true,
"workspaces": ["apps/*", "packages/*"],
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test",
"type-check": "turbo run type-check",
"clean": "turbo run clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^1.13.0"
}
}
Workspace Structure
Design the package structure for our monorepo.
We have: Next.js web app, Express API, React Native mobile app.
Share: UI components, API client, auth utilities, TypeScript config.
my-monorepo/
├── apps/
│ ├── web/ # Next.js — depends on @repo/ui, @repo/api-client
│ ├── api/ # Express — depends on @repo/auth
│ └── mobile/ # React Native — depends on @repo/ui (native), @repo/api-client
├── packages/
│ ├── ui/ # Shared React components (React + React Native)
│ ├── api-client/ # Typed API client generated from OpenAPI
│ ├── auth/ # Auth utilities, JWT validation
│ └── typescript-config/ # Shared tsconfig presets
├── turbo.json
└── package.json
// packages/typescript-config/package.json
{
"name": "@repo/typescript-config",
"version": "0.0.0",
"private": true,
"files": ["*.json"],
"exports": {
"./base.json": "./base.json",
"./nextjs.json": "./nextjs.json",
"./react-library.json": "./react-library.json"
}
}
// packages/typescript-config/base.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true
}
}
// apps/web/tsconfig.json — extends shared config
{
"extends": "@repo/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Internal Package Development
Create the @repo/ui package with a Button component
that works in both the Next.js web app and React Native mobile app.
// packages/ui/src/Button/index.tsx
// Platform-agnostic interface — implementations below
export type ButtonVariant = 'primary' | 'secondary' | 'ghost';
export type ButtonSize = 'sm' | 'md' | 'lg';
export interface ButtonProps {
label: string;
onPress: () => void;
variant?: ButtonVariant;
size?: ButtonSize;
disabled?: boolean;
loading?: boolean;
}
// packages/ui/src/Button/Button.web.tsx
import { ButtonProps } from './index';
const sizeClasses = { sm: 'px-3 py-1.5 text-sm', md: 'px-4 py-2 text-base', lg: 'px-6 py-3 text-lg' };
const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
ghost: 'text-blue-600 hover:bg-blue-50',
};
export function Button({ label, onPress, variant = 'primary', size = 'md', disabled, loading }: ButtonProps) {
return (
<button
onClick={onPress}
disabled={disabled || loading}
className={`${sizeClasses[size]} ${variantClasses[variant]} rounded-md font-medium transition-colors`}
>
{loading ? <span className="animate-spin">⟳</span> : label}
</button>
);
}
// packages/ui/src/Button/Button.native.tsx
import { TouchableOpacity, Text, ActivityIndicator, StyleSheet } from 'react-native';
import { ButtonProps } from './index';
export function Button({ label, onPress, variant = 'primary', disabled, loading }: ButtonProps) {
return (
<TouchableOpacity
onPress={onPress}
disabled={disabled || loading}
style={[styles.base, styles[variant], disabled && styles.disabled]}
>
{loading
? <ActivityIndicator color={variant === 'primary' ? '#fff' : '#2563eb'} />
: <Text style={[styles.label, variant !== 'primary' && styles.labelDark]}>{label}</Text>
}
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
base: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 8, alignItems: 'center' },
primary: { backgroundColor: '#2563eb' },
secondary: { backgroundColor: '#f3f4f6' },
ghost: { backgroundColor: 'transparent' },
disabled: { opacity: 0.5 },
label: { color: '#fff', fontWeight: '600' },
labelDark: { color: '#1f2937' },
});
// packages/ui/package.json
{
"name": "@repo/ui",
"version": "0.0.0",
"private": true,
"exports": {
"./button": {
"react-native": "./src/Button/Button.native.tsx",
"default": "./src/Button/Button.web.tsx"
}
},
"scripts": {
"build": "tsc",
"lint": "eslint src/",
"type-check": "tsc --noEmit"
},
"peerDependencies": {
"react": ">=18",
"react-native": ">=0.71"
},
"devDependencies": {
"@repo/typescript-config": "*"
}
}
Remote Caching
Set up remote caching so the CI cache is shared with local development.
We use Vercel as the remote cache.
# Authenticate with Vercel remote cache (one-time)
npx turbo login
npx turbo link
# Now builds use remote cache — first push fills it, subsequent pulls are instant
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20' }
- name: Install dependencies
run: npm ci
- name: Build, lint, and test (with remote cache)
run: turbo run build lint test type-check
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
# First CI run populates the remote cache
# Subsequent runs — even on other machines — restore from it
With remote caching, turbo run build on a PR that only touches apps/web restores the packages/* build outputs from cache and only rebuilds what changed. CI that took 8 minutes drops to under 2 minutes.
Running Specific Workspaces
# Build only the web app and its dependencies
turbo run build --filter=web
# Build everything that depends on the ui package (when you change it)
turbo run build --filter=...@repo/ui...
# Run tests in packages that changed since main
turbo run test --filter=[main]
# Run dev for only web and api
turbo run dev --filter=web --filter=api
For the Kubernetes deployment that runs the apps built in this pipeline, see the GitOps guide. For monorepo-specific CI patterns including affected file detection, see the GitHub Actions guide. The Claude Skills 360 bundle includes monorepo skill sets for Turborepo configuration, workspace patterns, and pnpm workspace setups. Start with the free tier to try monorepo scaffold generation.