Turborepo is a high-performance build system for JavaScript/TypeScript monorepos. It understands the dependency graph between workspace packages, runs tasks in the correct order, caches outputs locally and remotely, and replays builds that haven’t changed — often reducing CI times from 10 minutes to under 30 seconds. Claude Code configures Turborepo pipelines, sets up remote caching, designs workspace package boundaries, and writes the shared package code that connects your apps.
CLAUDE.md for Turborepo Monorepos
## Monorepo Stack
- Package manager: pnpm workspaces (pnpm-workspace.yaml)
- Build system: Turborepo 2.x
- Apps: apps/web (Next.js 15), apps/api (Fastify), apps/docs (Astro)
- Packages: packages/ui (shared components), packages/db (Drizzle schema), packages/types (shared TS types), packages/config (eslint/ts configs)
- Remote cache: Vercel Remote Cache (free for personal)
- Node version: 22 LTS (pinned via .nvmrc and engines in root package.json)
- All packages: TypeScript with strict mode, no any without comment
turbo.json — Pipeline Configuration
{
"$schema": "https://turbo.build/schema.json",
"ui": "tui",
"tasks": {
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env.local"],
"outputs": [".next/**", "!.next/cache/**", "dist/**", "dist-types/**"],
"env": ["NODE_ENV", "NEXT_PUBLIC_API_URL"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"typecheck": {
"dependsOn": ["^typecheck"],
"inputs": ["**/*.ts", "**/*.tsx", "tsconfig.json", "tsconfig.*.json"]
},
"lint": {
"inputs": ["**/*.ts", "**/*.tsx", ".eslintrc.*", "eslint.config.*"]
},
"test": {
"dependsOn": ["^build"],
"inputs": ["**/*.ts", "**/*.tsx", "**/*.test.ts", "vitest.config.*"],
"outputs": ["coverage/**"],
"env": ["DATABASE_URL", "REDIS_URL"]
},
"test:e2e": {
"dependsOn": ["build"],
"cache": false,
"env": ["PLAYWRIGHT_BASE_URL"]
},
"db:generate": {
"inputs": ["packages/db/src/schema/**"],
"outputs": ["packages/db/drizzle/**"]
},
"db:migrate": {
"dependsOn": ["db:generate"],
"cache": false
}
},
"globalDependencies": [
".env",
"tsconfig.base.json"
],
"globalEnv": ["CI", "VERCEL_ENV"]
}
pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "tooling/*"
Workspace Package: packages/db
// packages/db/package.json
{
"name": "@myapp/db",
"version": "0.0.0",
"private": true,
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
},
"./schema": {
"import": "./dist/schema.mjs",
"require": "./dist/schema.cjs",
"types": "./dist/schema.d.ts"
}
},
"scripts": {
"build": "tsup src/index.ts src/schema.ts --format esm,cjs --dts",
"generate": "drizzle-kit generate",
"migrate": "drizzle-kit migrate",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"drizzle-orm": "^0.38.0",
"@neondatabase/serverless": "^0.10.0"
},
"devDependencies": {
"drizzle-kit": "^0.30.0",
"tsup": "^8.0.0",
"@myapp/typescript-config": "workspace:*"
}
}
// packages/db/src/schema.ts — shared schema used by all apps
import { pgTable, text, integer, timestamp, decimal, pgEnum } from 'drizzle-orm/pg-core';
export const orderStatusEnum = pgEnum('order_status', [
'pending', 'processing', 'shipped', 'delivered', 'cancelled'
]);
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const orders = pgTable('orders', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
userId: text('user_id').notNull().references(() => users.id),
status: orderStatusEnum('status').default('pending').notNull(),
totalCents: integer('total_cents').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
// packages/db/src/index.ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
export function createDb(databaseUrl: string) {
const sql = neon(databaseUrl);
return drizzle(sql, { schema });
}
export type Database = ReturnType<typeof createDb>;
export { schema };
export * from './schema';
Workspace Package: packages/ui
// packages/ui/src/button.tsx — shared UI component
import { type ButtonHTMLAttributes, forwardRef } from 'react';
import { cn } from './utils';
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', loading, children, className, disabled, ...props }, ref) => {
const base = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50';
const variants = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-600',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-500',
danger: 'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-600',
ghost: 'hover:bg-gray-100 text-gray-700 focus-visible:ring-gray-500',
};
const sizes = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
};
return (
<button
ref={ref}
className={cn(base, variants[variant], sizes[size], className)}
disabled={disabled || loading}
{...props}
>
{loading && (
<svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
)}
{children}
</button>
);
}
);
Button.displayName = 'Button';
Consuming Packages in Apps
// apps/web/app/orders/page.tsx — imports from workspace packages
import { createDb } from '@myapp/db';
import { orders, users } from '@myapp/db/schema';
import { Button } from '@myapp/ui';
import type { Order } from '@myapp/types';
import { eq, desc } from 'drizzle-orm';
const db = createDb(process.env.DATABASE_URL!);
export default async function OrdersPage() {
const userOrders = await db.query.orders.findMany({
orderBy: desc(orders.createdAt),
limit: 20,
with: { user: true },
});
return (
<div>
<h1>Orders</h1>
{userOrders.map(order => (
<div key={order.id}>{order.id}</div>
))}
<Button variant="primary">New Order</Button>
</div>
);
}
Pruned Docker Builds
# Turborepo prune: generate minimal lockfile for one app
# turbo prune apps/api --docker
# Stage 1: Prune
FROM node:22-alpine AS pruner
RUN npm i -g turbo@2
WORKDIR /app
COPY . .
RUN turbo prune apps/api --docker
# Stage 2: Install dependencies (cached layer)
FROM node:22-alpine AS installer
RUN corepack enable pnpm
WORKDIR /app
COPY --from=pruner /app/out/json/ .
COPY --from=pruner /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
RUN pnpm install --frozen-lockfile
# Stage 3: Build
FROM node:22-alpine AS builder
RUN corepack enable pnpm
WORKDIR /app
COPY --from=installer /app .
COPY --from=pruner /app/out/full/ .
RUN pnpm turbo build --filter=apps/api
# Stage 4: Runner
FROM node:22-alpine AS runner
WORKDIR /app
COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/apps/api/package.json .
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
EXPOSE 3000
CMD ["node", "dist/server.js"]
Remote Caching in CI
# .github/workflows/ci.yml
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build, lint, test
run: pnpm turbo build lint typecheck test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} # Vercel Remote Cache token
TURBO_TEAM: ${{ vars.TURBO_TEAM }} # Your Vercel team slug
For the Drizzle ORM schema shared across workspace packages, see the Drizzle ORM guide for migration and query patterns. For containerizing monorepo apps with Docker, the Docker guide covers multi-stage builds that pair with Turborepo prune. The Claude Skills 360 bundle includes Turborepo skill sets covering pipeline configuration, workspace packages, and remote caching. Start with the free tier to try monorepo pipeline generation.