Softechinfra
Development

TypeScript Monorepo Guide: Turborepo, Nx, and pnpm

Build scalable TypeScript monorepos with Turborepo or Nx—setup, configuration, caching, and CI/CD best practices.

Hrishikesh BaidyaHrishikesh Baidya
March 1, 202511 min read
TypeScript Monorepo Guide: Turborepo, Nx, and pnpm

Monorepos have become the architecture of choice for TypeScript projects at scale. As Hrishikesh Baidya, our CTO, notes: "A well-structured monorepo lets teams move faster—shared code, atomic commits, and unified tooling pay dividends as you scale." At Softechinfra, we've built multiple production monorepos for complex SaaS applications.

60%
Faster Builds w/Caching
40%
Less Code Duplication
3x
Faster Refactoring
1
Setup, All Projects

Why Monorepos for TypeScript?

🔗
Code Sharing
Shared types, utilities, and components across all packages—no npm publish needed
⚛️
Atomic Changes
Update a shared type and all consumers in one commit—no version coordination
🛠️
Unified Tooling
Single ESLint, Prettier, and TypeScript config for consistency
🧑‍💻
Developer Experience
One git clone, one install, full codebase—no context switching

Tool Comparison: Turborepo vs Nx

Feature Turborepo Nx
Setup Complexity Simple, minimal config More configuration options
Build Caching Excellent, hash-based Excellent, computation caching
Affected Commands --filter flag Built-in affected:*
Remote Caching Vercel (free tier) Nx Cloud (free tier)
Best For Getting started, medium teams Large orgs, enterprise needs
Our Recommendation: Start with Turborepo + pnpm for most projects. Switch to Nx if you need generators, plugins ecosystem, or enterprise features. Both work great for TypeScript.

Project Setup

Directory Structure

code
my-monorepo/
  ├── apps/
  │   ├── web/              # Next.js frontend
  │   ├── api/              # Express/Fastify backend
  │   └── admin/            # Admin dashboard
  ├── packages/
  │   ├── ui/               # Shared React components
  │   ├── shared/           # Shared types and utilities
  │   ├── database/         # Prisma schema and client
  │   └── config/           # Shared configs (tsconfig, eslint)
  ├── package.json
  ├── pnpm-workspace.yaml
  ├── turbo.json
  └── tsconfig.json

Workspace Configuration

1
pnpm-workspace.yaml
Define which directories contain packages:
yaml
packages:
    - 'apps/*'
    - 'packages/*'
2
turbo.json
Configure the build pipeline with dependencies:
json
{
    "$schema": "https://turbo.build/schema.json",
    "pipeline": {
      "build": {
        "dependsOn": ["^build"],
        "outputs": ["dist/**", ".next/**"]
      },
      "dev": {
        "cache": false,
        "persistent": true
      },
      "test": {
        "dependsOn": ["build"],
        "outputs": ["coverage/**"]
      },
      "lint": {
        "outputs": []
      }
    }
  }

TypeScript Configuration

Shared Base Config

packages/config/tsconfig.base.json:

json
{
    "compilerOptions": {
      "strict": true,
      "esModuleInterop": true,
      "skipLibCheck": true,
      "forceConsistentCasingInFileNames": true,
      "declaration": true,
      "declarationMap": true,
      "sourceMap": true,
      "moduleResolution": "bundler",
      "target": "ES2022",
      "lib": ["ES2022"]
    }
  }

Package Config Extension

apps/web/tsconfig.json:

json
{
    "extends": "@myorg/config/tsconfig.base.json",
    "compilerOptions": {
      "outDir": "dist",
      "rootDir": "src",
      "jsx": "react-jsx"
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
  }

Internal Package Patterns

"The key to monorepo success is treating internal packages like real packages—proper exports, TypeScript declarations, and clear boundaries. Don't let the monorepo become a tangled mess of imports."
HB
Hrishikesh Baidya CTO, Softechinfra

Shared Package Setup

packages/shared/package.json:

json
{
    "name": "@myorg/shared",
    "version": "0.0.0",
    "private": true,
    "main": "./dist/index.js",
    "types": "./dist/index.d.ts",
    "exports": {
      ".": {
        "types": "./dist/index.d.ts",
        "import": "./dist/index.js"
      }
    },
    "scripts": {
      "build": "tsc",
      "dev": "tsc --watch"
    }
  }

Shared Types Example

packages/shared/src/types.ts:

typescript
// Domain types shared across apps
  export interface User {
    id: string
    email: string
    name: string
    role: 'admin' | 'user' | 'viewer'
  }
  
  export interface ApiResponse<T> {
    data: T
    meta: {
      requestId: string
      timestamp: number
    }
  }
  
  // Utility types
  export type Nullable<T> = T | null
  export type AsyncFunction<T> = () => Promise<T>

Using Internal Packages

apps/web/package.json:

json
{
    "dependencies": {
      "@myorg/shared": "workspace:*",
      "@myorg/ui": "workspace:*"
    }
  }

Build Optimization

Caching Strategies

💾
Local Cache
Turborepo caches in.turbo by default—subsequent builds skip unchanged packages
☁️
Remote Cache
Share cache across CI and team members—huge speedup for large repos
🎯
Affected Builds
Only rebuild packages affected by changes—critical for CI efficiency
⚡
Parallel Execution
Turborepo automatically parallelizes independent tasks

Affected Builds Command

bash
# Build only packages affected since last commit
  turbo run build --filter=[HEAD^1]
  
  # Build only packages affected compared to main
  turbo run build --filter=[origin/main...]
  
  # Build specific package and its dependencies
  turbo run build --filter=@myorg/web...

CI/CD Configuration

GitHub Actions with Remote Caching

yaml
name: CI
  on: [push, pull_request]
  
  jobs:
    build:
      runs-on: ubuntu-latest
      steps:
        - uses: actions/checkout@v4
          with:
            fetch-depth: 2  # Needed for affected commands
  
        - uses: pnpm/action-setup@v3
          with:
            version: 8
  
        - uses: actions/setup-node@v4
          with:
            node-version: 20
            cache: 'pnpm'
  
        - run: pnpm install --frozen-lockfile
  
        # Remote caching with Vercel
        - run: pnpm turbo build test lint --filter=[HEAD^1]
          env:
            TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
            TURBO_TEAM: ${{ vars.TURBO_TEAM }}
CI Tip: Always use `--frozen-lockfile` in CI to ensure reproducible builds. Set up remote caching early—it pays dividends as your repo grows. See our secure development guide for CI security practices.

Common Patterns

Checklist for Monorepo Success

  • Keep packages small and focused—single responsibility
  • Use workspace:* for internal dependencies
  • Share ESLint/Prettier configs via internal package
  • Set up remote caching from day one
  • Use TypeScript project references for faster type checking
  • Document package purposes in README files

When to Split Packages

    Good candidates for separate packages:
  • UI components — shared across multiple apps
  • API clients — generated or hand-written
  • Utility functions — date helpers, validators
  • Types/interfaces — domain models
  • Config files — tsconfig, eslint, prettier

Projects like ChipMakerHub demonstrate this pattern with shared UI components across customer and admin portals.

Need Help Setting Up Your Monorepo?

We help teams architect and implement TypeScript monorepos that scale—from initial setup to CI/CD optimization.

Discuss Your Architecture
Tags:
TypeScriptMonorepoDevelopmentArchitectureTurborepo
Share this post:
Hrishikesh Baidya

Hrishikesh Baidya

CTO at Softechinfra specializing in Python, system architecture, and building secure, scalable software solutions.