Claude Code for Turborepo: Monorepo Pipelines, Caching, and Remote Cache — Claude Skills 360 Blog
Blog / DevOps / Claude Code for Turborepo: Monorepo Pipelines, Caching, and Remote Cache
DevOps

Claude Code for Turborepo: Monorepo Pipelines, Caching, and Remote Cache

Published: November 23, 2026
Read time: 8 min read
By: Claude Skills 360

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.

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free