Why Monorepos Are Worth the Setup in 2026

If your engineering team is managing a frontend app, a backend API, a shared component library, and a set of utility packages across separate repositories, you already know the pain: version drift, duplicated configs, broken cross-package changes that only surface in production, and a CI pipeline that takes forever because nothing is cached.

Monorepos solve this — and in 2026, Turborepo has become the go-to tool for TypeScript-first monorepos in the JavaScript ecosystem. It ships with remote caching, incremental builds, and a pipeline model that integrates cleanly with GitHub Actions, Vercel, and most cloud providers used by teams in Australia, Singapore, Canada, and the US.

This guide walks you through setting up a production-ready TypeScript monorepo from scratch using Turborepo 2.x, with shared packages, proper path aliases, and a working build pipeline.

What You'll Need

  • Node.js 20+ (LTS recommended)
  • pnpm 9+ — Turborepo works best with pnpm workspaces
  • Basic familiarity with TypeScript and npm packages
  • A terminal and a code editor (VS Code or Cursor work great)

Why pnpm? pnpm's workspace protocol handles cross-package dependencies cleanly and its symlink strategy avoids the phantom dependency problems that plague npm and Yarn hoisting.

Step 1: Initialise the Monorepo

Start by creating a new directory and initialising a pnpm workspace.

mkdir my-monorepo
cd my-monorepo
pnpm init

Now create a pnpm-workspace.yaml at the root to declare where your packages live:

packages:
  - 'apps/*'
  - 'packages/*'

Then install Turborepo as a dev dependency at the root:

pnpm add -D turbo -w

Create the top-level directory structure:

mkdir -p apps/web apps/api packages/ui packages/utils packages/tsconfig

Pro tip: Keep your apps/ directory for deployable applications and packages/ for shared internal libraries. This separation makes it much easier to apply different deployment strategies to each.

Step 2: Configure the Root TypeScript and Turborepo Settings

Root tsconfig

Create a tsconfig.json at the root. This is a base config your packages will extend:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  }
}

Shared tsconfig package

Inside packages/tsconfig, create a package.json:

{
  "name": "@repo/tsconfig",
  "version": "0.0.1",
  "private": true,
  "files": ["*.json"]
}

Then create packages/tsconfig/base.json with your shared compiler options, and packages/tsconfig/nextjs.json and packages/tsconfig/node.json for app-specific variants. This pattern eliminates repeated TypeScript config across every app and package in your repo.

turbo.json

Create turbo.json at the root to define your pipeline:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "typecheck": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

The "^build" syntax tells Turborepo: before running build on this package, build all its dependencies first. This is what enables correct build ordering automatically.

Common pitfall: Forgetting to declare outputs breaks caching. Turborepo uses output hashing to decide whether a task needs to re-run. If your dist/ folder isn't listed, the cache will never restore it.

Step 3: Set Up a Shared UI Package

Navigate to packages/ui and initialise it:

{
  "name": "@repo/ui",
  "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",
    "dev": "tsc --watch",
    "lint": "eslint src/",
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "@repo/tsconfig": "workspace:*",
    "typescript": "^5.7.0"
  }
}

Add a simple packages/ui/src/index.ts to export your components. Because this is declared as a workspace package, any app can import from it using @repo/ui — and pnpm handles the symlink automatically.

Step 4: Create an App and Wire Up Internal Dependencies

Inside apps/web, set up a Next.js 15 app (or any framework you prefer). Add your internal packages as dependencies using the workspace protocol:

{
  "name": "web",
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/utils": "workspace:*"
  }
}

Run pnpm install from the root. pnpm will resolve the workspace packages and link them locally. You can now import directly:

import { Button } from '@repo/ui'

Pro tip: Use TypeScript path aliases sparingly in a monorepo. Let the exports field in each package's package.json do the heavy lifting instead — it's more explicit and plays better with bundlers like Vite, esbuild, and the Next.js compiler.

Step 5: Add a Root-Level Dev Script and Test the Pipeline

Add the following to your root package.json:

{
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "typecheck": "turbo run typecheck",
    "test": "turbo run test"
  }
}

Run pnpm build from the root. Turborepo will analyse the dependency graph, build shared packages first, then build the apps that depend on them — in parallel where possible.

On the second run, you'll see the magic: tasks that haven't changed are restored from cache instantly. For a mid-sized team managing three or four apps, this alone can cut CI time by 60–70%.

Step 6: Enable Remote Caching

Local caching is great, but remote caching is what makes Turborepo exceptional for teams. Link your repo to Vercel's remote cache (free for most teams):

pnpm dlx turbo login
pnpm dlx turbo link

Once linked, every developer on your team — and your CI runner — shares the same cache. If a colleague already built the @repo/ui package on their machine, you won't have to rebuild it in CI.

If you prefer a self-hosted option, Turborepo 2.x supports any S3-compatible storage backend as a custom remote cache endpoint, which matters for teams with strict data residency requirements in Singapore or Australia.

Common pitfall: Don't commit your .turbo directory or Turbo token to version control. Add .turbo to your .gitignore and store the TURBO_TOKEN as a CI secret.

Step 7: Wire Up GitHub Actions CI

Create .github/workflows/ci.yml:

name: CI
on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - run: pnpm build
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
      - run: pnpm typecheck
      - run: pnpm lint
      - run: pnpm test

With remote caching wired in, pull requests that only touch the apps/web package won't rebuild @repo/ui or @repo/utils — Turborepo skips them automatically.

Common Pitfalls to Avoid

  • Circular dependencies: If @repo/ui imports from @repo/utils and vice versa, Turborepo's pipeline will deadlock. Keep shared packages strictly leaf nodes in your dependency graph.
  • Missing build outputs in turbo.json: Any file that a downstream package depends on must be listed as an output — otherwise caching will serve stale or empty builds.
  • Using npm or Yarn hoisting with Turborepo: Both can produce phantom dependency issues that are difficult to debug. Stick with pnpm workspaces.
  • Forgetting --frozen-lockfile in CI: Without it, CI might silently upgrade packages and produce builds that don't match local development.

Next Steps

Once your monorepo is running smoothly, the next natural steps are:

  • Add Changesets for versioning and changelogs if you plan to publish any packages to a private npm registry
  • Set up Vitest with a shared config package for consistent unit testing across all workspaces
  • Introduce ESLint 9's flat config at the root level so every package inherits the same linting rules without duplication
  • Explore Turborepo's task graph visualiser (turbo run build --graph) to audit your pipeline and catch bottlenecks early

If your team is starting a new product or migrating an existing multi-repo setup, getting the monorepo architecture right from the beginning saves weeks of rework later. At Lenka Studio, we've set up Turborepo-based monorepos for SaaS products and ecommerce platforms across Australia and Southeast Asia — and the consistency gains across frontend, backend, and shared tooling are immediately felt by the whole team.

If you're not sure whether a monorepo is the right architecture for where your product is headed, or you want a second opinion on your current setup, get in touch with the Lenka Studio team. We're happy to talk through the trade-offs with you.