ZyVOP Logo
Content That Connects
SeriesCategoriesTags
ZyVOP Logo
Content That Connects

Empowering developers and creators with cutting-edge insights, comprehensive tutorials, and innovative solutions for the digital future.

Content

  • Tags
  • Write Article

Company

  • About Us
  • Contact

Connect

  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • DMCA Policy
  • Code of Conduct

© 2026 ZyVOP. Crafted with care for the developer community.

Made with ❤️ by the ZyVOP team
All systems operational
HomeTypeScript Monorepos with Turborepo and pnpm: The Setup That Actually Works

TypeScript Monorepos with Turborepo and pnpm: The Setup That Actually Works

Build a scalable TypeScript monorepo with pnpm workspaces, Turborepo caching, shared packages, incremental builds, and production-ready Docker workflows.

#TypeScript monorepo 2026#Turborepo pnpm workspaces#monorepo setup Node.js#Turborepo pipeline#TypeScript project references monorepo#pnpm workspace tutorial#monorepo Docker build
Z
ZyVOP

Senior Developer

May 27, 2026
7 min read
2 views
TypeScript Monorepos with Turborepo and pnpm: The Setup That Actually Works

The promise of a monorepo is simple: one repository, shared code, unified tooling, atomic changes across packages. The reality, historically, was a fragile tangle of symlinks, build script order-of-operations problems, and TypeScript errors that only appeared in CI.

The tooling has caught up. In 2026, pnpm workspaces + Turborepo + TypeScript Project References is the stack that actually works — fast, well-documented, and production-tested at scale. Monorepos are now used by 63% of companies with 50+ developers, and for JavaScript/TypeScript teams managing between 5 and 50 packages, Turborepo is the practical choice.

This guide builds the full setup from scratch: workspace structure, shared packages, TypeScript project references, Turborepo pipelines, and Docker builds that work correctly with monorepo dependencies.


When a Monorepo Makes Sense

Before setting one up, be honest about whether you need it.

Use a monorepo when:

  • You have shared code (types, utilities, UI components) used by multiple apps

  • You make changes that span multiple packages atomically (API contract change + client update)

  • You are tired of version drift between a shared library and the apps that consume it

  • Your team works across packages regularly and context-switching between repos is friction

Stick with separate repos when:

  • Each service is truly independent with no shared code

  • Different teams own different services and need independent deployment cadences

  • You have fewer than 3 packages — the overhead is not worth it yet


The Structure

/
├── apps/
│   ├── api/           # Node.js Express API
│   └── web/           # Frontend (Next.js, etc.)
├── packages/
│   ├── types/         # Shared TypeScript types
│   ├── utils/         # Shared utility functions
│   ├── ui/            # Shared UI components (if applicable)
│   └── config/        # Shared ESLint, TypeScript configs
├── package.json       # Root — workspace definition
├── pnpm-workspace.yaml
└── turbo.json

Step 1 — Initialize the Workspace

# Install pnpm if you haven't
npm install -g pnpm

mkdir my-monorepo && cd my-monorepo
pnpm init
# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'
// package.json (root)
{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build":   "turbo build",
    "dev":     "turbo dev",
    "lint":    "turbo lint",
    "test":    "turbo test",
    "typecheck": "turbo typecheck"
  },
  "devDependencies": {
    "turbo":      "latest",
    "typescript": "^5.5.0"
  },
  "engines": {
    "node": ">=20",
    "pnpm": ">=9"
  }
}
pnpm add -D turbo -w   # -w installs to workspace root

Step 2 — Shared Config Package

Start with the config package — everything else depends on it.

mkdir -p packages/config
cd packages/config
pnpm init
// packages/config/package.json
{
  "name": "@myrepo/config",
  "version": "0.0.1",
  "private": true,
  "exports": {
    "./tsconfig":   "./tsconfig.base.json",
    "./tsconfig/*": "./tsconfig.*.json"
  }
}
// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "target":          "ES2022",
    "module":          "Node16",
    "moduleResolution":"Node16",
    "lib":             ["ES2022"],
    "strict":          true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess":   true,
    "declaration":      true,
    "declarationMap":   true,
    "sourceMap":        true,
    "esModuleInterop":  true,
    "skipLibCheck":     true
  }
}
// packages/config/tsconfig.node.json — for Node.js packages
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "outDir":  "dist",
    "rootDir": "src"
  },
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}

Step 3 — Shared Types Package

mkdir -p packages/types/src
// packages/types/package.json
{
  "name":    "@myrepo/types",
  "version": "0.0.1",
  "private": true,
  "main":    "./dist/index.js",
  "types":   "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types":  "./dist/index.d.ts"
    }
  },
  "scripts": {
    "build":     "tsc",
    "typecheck": "tsc --noEmit",
    "dev":       "tsc --watch"
  },
  "devDependencies": {
    "@myrepo/config": "workspace:*",
    "typescript":     "^5.5.0"
  }
}
// packages/types/tsconfig.json
{
  "extends": "@myrepo/config/tsconfig",
  "compilerOptions": {
    "outDir":  "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}
// packages/types/src/index.ts
export interface User {
  id:        string;
  email:     string;
  fullName:  string;
  role:      'user' | 'admin';
  createdAt: Date;
}

export interface Order {
  id:        string;
  userId:    string;
  status:    'pending' | 'paid' | 'shipped' | 'cancelled';
  total:     number;
  createdAt: Date;
}

export interface ApiResponse<T> {
  data:  T;
  meta?: Record<string, unknown>;
}

export interface PaginatedResponse<T> {
  data:       T[];
  pagination: {
    hasNextPage: boolean;
    nextCursor:  string | null;
  };
}

Step 4 — The API App

mkdir -p apps/api/src
// apps/api/package.json
{
  "name":    "@myrepo/api",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "build":     "tsc",
    "dev":       "tsx watch src/server.ts",
    "start":     "node dist/server.js",
    "typecheck": "tsc --noEmit",
    "test":      "vitest run"
  },
  "dependencies": {
    "@myrepo/types":  "workspace:*",
    "@myrepo/utils":  "workspace:*",
    "express":        "^4.21.0"
  },
  "devDependencies": {
    "@myrepo/config": "workspace:*",
    "typescript":     "^5.5.0",
    "tsx":            "^4.0.0"
  }
}
// apps/api/tsconfig.json
{
  "extends": "@myrepo/config/tsconfig",
  "compilerOptions": {
    "outDir":  "./dist",
    "rootDir": "./src",
    // Project references — tells TypeScript about dependencies
    // Enables incremental builds — only rebuilds what changed
  },
  "references": [
    { "path": "../../packages/types" },
    { "path": "../../packages/utils" }
  ],
  "include": ["src"]
}
// apps/api/src/server.ts
import express from 'express';
import type { User } from '@myrepo/types';   // From the shared types package
import { formatDate } from '@myrepo/utils';   // From the shared utils package

const app = express();
app.use(express.json());

app.get('/users/:id', async (req, res) => {
  const user: User = await getUserById(req.params.id);
  res.json({ data: { ...user, createdAt: formatDate(user.createdAt) } });
});

app.listen(3000);

Step 5 — Turborepo Pipeline

// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": ["**/.env.*local"],
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],   // ^ means: build dependencies first
      "outputs":   ["dist/**"],
      "inputs":    ["src/**", "tsconfig.json", "package.json"]
    },
    "dev": {
      "dependsOn": ["^build"],   // Ensure deps are built before dev server starts
      "cache":      false,       // Never cache dev
      "persistent": true         // Long-running process
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs":   ["coverage/**"],
      "inputs":    ["src/**", "tests/**"]
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs":   []
    },
    "lint": {
      "outputs": []
    }
  }
}

The ^build in dependsOn is the key: it tells Turborepo "before running this task, run build in all dependency packages." So when you build @myrepo/api, Turborepo automatically builds @myrepo/types and @myrepo/utils first. In parallel where possible.

Running tasks:

# Build everything in the right order
pnpm turbo build

# Build only the API and its dependencies
pnpm turbo build --filter=@myrepo/api

# Run dev servers for all apps in parallel
pnpm turbo dev

# Only run what changed since main branch
pnpm turbo test --filter=...[origin/main]

# Run with remote caching (Vercel)
pnpm turbo build --token=$TURBO_TOKEN --team=$TURBO_TEAM

Step 6 — Docker Build for Production

The tricky part with monorepos and Docker is that COPY . . copies the entire monorepo into the build context. For a large monorepo this is slow and the resulting image is huge. Turborepo's prune command solves this — it generates a minimal subset of the monorepo with only the files needed for a specific app.

# Generate a pruned version of the monorepo for the API
npx turbo prune @myrepo/api --docker

This creates an out/ directory with:

  • out/json/ — just the package.json files (for dependency installation)

  • out/full/ — the complete pruned source

# apps/api/Dockerfile
FROM node:20-alpine AS base
RUN npm install -g pnpm

# Stage 1: Install dependencies (cached unless package.json changes)
FROM base AS deps
WORKDIR /app
# Copy only package.json files first — layer cache for dependency install
COPY out/json/ .
COPY pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile

# Stage 2: Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY out/full/ .
RUN pnpm turbo build --filter=@myrepo/api

# Stage 3: Production image
FROM node:20-alpine AS production
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app

COPY --from=builder /app/apps/api/dist ./dist
COPY --from=builder /app/apps/api/package.json ./
COPY --from=builder /app/node_modules ./node_modules

USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]

Build it:

# From the repo root
npx turbo prune @myrepo/api --docker
docker build -f apps/api/Dockerfile -t myrepo-api .

Step 7 — CI Pipeline

# .github/workflows/ci.yml
name: CI

on: [push, pull_request]

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2    # Needed for --filter=[HEAD^1] to work

      - uses: pnpm/action-setup@v3
        with:
          version: 9

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

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

      - name: Typecheck
        run: pnpm turbo typecheck

      - name: Lint
        run: pnpm turbo lint

      - name: Test
        run: pnpm turbo test

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

With remote caching enabled, Turborepo skips tasks whose inputs have not changed since the last run — even across different machines. Remote caching can make builds up to 10x faster, which matters in CI where you pay per minute.


Adding a New Package

The pattern for adding any new shared package:

mkdir -p packages/logger/src
cd packages/logger

# Create package.json with name @myrepo/logger
# Create tsconfig.json extending @myrepo/config/tsconfig
# Write your code in src/

# Install it in apps that need it
cd ../../apps/api
pnpm add @myrepo/logger --workspace

That's it. pnpm handles the symlink. Turborepo detects the dependency from package.json and includes it in the build graph automatically.

Z

ZyVOP

Passionate developer sharing knowledge about modern web technologies and best practices.

Comments (0)

Login to post a comment.

Stay Updated

Get the latest articles delivered to your inbox.

We respect your privacy. Unsubscribe anytime.

Popular Tags

#.env.example Node.js#0x profiling#12-factor#AI agents#AI code security#AI coding tools 2026#AI-assisted development#AI-generated vulnerabilities#ALTER TABLE no lock#API Design