DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Turborepo Monorepo with Claude Code: Shared Types, Build Cache, and pnpm Workspace

Managing frontend and backend in separate repositories sounds clean — until you change an API response shape and spend two hours syncing type definitions across three repos.

Turborepo consolidates everything into a single repository with intelligent build caching. Claude Code reads your CLAUDE.md and generates the entire monorepo design from scratch. Here's how it works.


What You Tell Claude Code (CLAUDE.md)

## Monorepo Structure

- apps/ — deployable applications
  - api/ — Express backend
  - web/ — Next.js frontend
  - admin/ — React admin panel
- packages/ — shared internal libraries
  - types/ — shared TypeScript types (API contracts)
  - ui/ — shared UI components
  - config/ — shared ESLint, TypeScript configs

## Turbo Pipeline Rules

- ^build means upstream packages must build first before this app builds
- outputs specify what directories Turbo should cache
- CI: use remote cache + --filter to build only affected packages
- dev tasks are persistent (long-running, never complete)

## Dependency Protocol

- packages/types contains all shared API types (request/response interfaces)
- Apps depend on packages/types using workspace:* protocol
- Never duplicate type definitions across apps
Enter fullscreen mode Exit fullscreen mode

This is your contract with Claude Code. It reads this before generating any structure, configuration, or code.


turbo.json: The Pipeline Definition

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": [],
      "cache": false
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The key insight: "dependsOn": ["^build"] means packages/types builds before apps/api or apps/web. Turbo resolves the dependency graph automatically.

cache: false for test ensures tests always run fresh. persistent: true for dev tells Turbo these are long-running processes that don't exit.


pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'packages/*'
Enter fullscreen mode Exit fullscreen mode

Two lines. That's the entire workspace configuration. pnpm discovers every package under apps/ and packages/ automatically.


Shared Types Package

// packages/types/src/user.ts
export interface User {
  id: string;
  email: string;
  displayName: string;
  createdAt: string;
  role: 'admin' | 'member' | 'viewer';
}

export interface UserListResponse {
  users: User[];
  total: number;
  page: number;
  pageSize: number;
}
Enter fullscreen mode Exit fullscreen mode
// packages/types/package.json
{
  "name": "@myapp/types",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch"
  }
}
Enter fullscreen mode Exit fullscreen mode

The @myapp/types package compiles TypeScript and exports both JavaScript and type definitions. Any app that installs this package gets full type safety.


Consuming Types in Apps

// apps/api/package.json (relevant section)
{
  "dependencies": {
    "@myapp/types": "workspace:*"
  }
}
Enter fullscreen mode Exit fullscreen mode

workspace:* tells pnpm to link directly to the local package instead of downloading from npm. When you change a type in packages/types, every app sees the change immediately after rebuilding.

// apps/api/src/routes/users.ts
import type { UserListResponse } from '@myapp/types';

export async function getUserList(req: Request, res: Response) {
  const users = await db.user.findMany({ take: 20 });

  const response: UserListResponse = {
    users,
    total: await db.user.count(),
    page: 1,
    pageSize: 20,
  };

  res.json(response);
}
Enter fullscreen mode Exit fullscreen mode
// apps/web/src/hooks/useUsers.ts
import type { UserListResponse } from '@myapp/types';

export function useUsers() {
  return useSWR<UserListResponse>('/api/users');
}
Enter fullscreen mode Exit fullscreen mode

Both the API and the web app use the same UserListResponse type. If you add a field to User, TypeScript will catch every place that needs to be updated — across both apps, in one terminal window.


GitHub Actions: CI with Remote Cache

name: CI

on:
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v2
        with:
          version: 8

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build
        run: pnpm turbo build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Test (affected only)
        run: pnpm turbo test --filter='[HEAD^1]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Enter fullscreen mode Exit fullscreen mode

TURBO_TOKEN enables Turbo's remote cache. When the same build runs again with identical inputs, Turbo downloads the cached dist/ instead of rebuilding. On large monorepos, this cuts CI time by 60-80%.

--filter='[HEAD^1]' tells Turbo to only run tests for packages that changed since the previous commit. If you only edited apps/web, the API tests don't run.


What Claude Code Actually Generates From This

When you hand Claude Code the CLAUDE.md above and say "scaffold this monorepo," it produces:

  • Complete directory structure with all six packages
  • turbo.json with correct dependsOn chains
  • pnpm-workspace.yaml
  • packages/types/ with TypeScript config, build scripts, and initial type definitions
  • packages/config/ with shared tsconfig.base.json and ESLint config
  • Each app's package.json with workspace:* references
  • A root package.json with turbo scripts

The CLAUDE.md makes it consistent. Without it, Claude Code might put types directly in apps/api, or forget the ^build dependency, or use npm instead of pnpm.


Summary

Piece Purpose
CLAUDE.md Tells Claude Code the exact structure and rules
packages/types Single source of truth for all API contracts
workspace:* Links packages locally without npm publishing
turbo.json pipeline Correct build order + aggressive caching
--filter='[HEAD^1]' CI runs only affected packages

The result: one git clone, one pnpm install, one pnpm dev to run everything. Type changes propagate instantly. CI is fast because of remote cache and filtered runs.


If you want to go deeper on Claude Code workflow design — including how to structure CLAUDE.md for different project types — my Code Review Pack covers multi-file review prompts and project configuration patterns.

Code Review Pack (¥980) — available at prompt-works.jp under /code-review

What's your current monorepo setup? Turborepo, Nx, or still separated repos?

Top comments (0)