How to Set Up a Monorepo with Turborepo and pnpm: A Practical Developer Tutorial

1 comment
(Developer Tutorials) - Stop juggling multiple repos and broken cross-package dependencies. Here's a step-by-step guide to building a scalable monorepo with Turborepo and pnpm — the exact setup we use with our remote teams in Vietnam.

How to Set Up a Monorepo with Turborepo and pnpm: A Practical Developer Tutorial

I’ve worked on projects where every shared library lived in its own Git repo. It was a nightmare. You’d fix a bug in `ui-components`, bump the version, publish to npm, then update three other repos. Rinse and repeat. That’s the kind of friction that kills developer velocity.

Monorepos fix this. One repo, one version, one `git log`. But without the right tooling, monorepos become slow and brittle. That’s where Turborepo and pnpm come in.

How to Turn Your Open Source Project Into a Revenue Stream with GitHub Sponsors (A Practical Guide)

How to Turn Your Open Source Project Into a Revenue Stream with GitHub Sponsors (A Practical Guide)

How to Turn Your Open Source Project Into a Revenue Stream with GitHub Sponsors (A Practical Guide) I’ve… ...

I’m going to walk you through the exact setup we use with our engineering teams in Ho Chi Minh City and Can Tho. It’s battle-tested, it’s fast, and it’ll save you hours every week.

Why Turborepo + pnpm?

You’ve got options: Nx, Lerna, Rush, Bazel. I’ve tried them all. For most teams, Turborepo hits the sweet spot between power and simplicity. It’s built by Vercel, it’s open source, and it’s dumb fast thanks to incremental builds and remote caching.

Why Vietnam Outsourcing Is the Smartest Move Your Tech Team Can Make in 2025

Why Vietnam Outsourcing Is the Smartest Move Your Tech Team Can Make in 2025

TL;DR: Vietnam outsourcing delivers enterprise-grade developers at 60% lower cost than the US, with a rapidly maturing ecosystem… ...

pnpm, on the other hand, is the package manager of choice. Unlike npm or Yarn, pnpm uses a content-addressable store. That means you never download the same dependency twice. Disk usage drops by 60-80% in my experience. And with pnpm workspaces, you get strict dependency isolation — no more “it works on my machine” because of hoisted phantom dependencies.

**Honestly?** If you’re still using npm workspaces, you’re leaving performance on the table.

Prerequisites

  • Node.js 18 or later
  • pnpm 8+ (install with `npm install -g pnpm`)
  • A GitHub account (for CI later)

Let’s build it.

Step 1: Initialize the Monorepo

Create a new directory and initialize the workspace.

bash
mkdir my-monorepo
cd my-monorepo
pnpm init

Now create a `pnpm-workspace.yaml` file at the root. This tells pnpm which directories contain packages.

yaml
packages:
  - "apps/*"
  - "packages/*"

Our structure will be:


my-monorepo/
  apps/
    web/          # Next.js app
    api/          # Express API
  packages/
    ui/           # Shared React components
    config/       # Shared ESLint, TypeScript configs
    utils/        # Shared utility functions
  pnpm-workspace.yaml
  package.json
  turbo.json

Step 2: Install Turborepo

Add Turborepo as a dev dependency at the root:

bash
pnpm add -D turbo

Create a `turbo.json` file. This is where the magic happens. Turborepo uses a pipeline to define task dependencies and caching rules.

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

Key points:

  • `”dependsOn”: [“^build”]` means a package’s `build` waits for its dependencies’ `build` to finish first.
  • `outputs` tells Turborepo which files to cache. If the source hasn’t changed, it skips the task entirely.
  • `”dev”` is marked `persistent` because dev servers don’t exit.

Step 3: Create the First Package

Let’s create a shared utilities package. Inside `packages/utils/`:

bash
mkdir packages/utils
cd packages/utils
pnpm init

Edit `packages/utils/package.json`:

json
{
  "name": "@myrepo/utils",
  "version": "0.0.1",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsup src/index.ts --dts",
    "test": "vitest run"
  },
  "devDependencies": {
    "tsup": "^8.0.0",
    "typescript": "^5.4.0",
    "vitest": "^1.6.0"
  }
}

Create `packages/utils/tsconfig.json`:

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "declaration": true,
    "outDir": "dist",
    "strict": true
  },
  "include": ["src"]
}

Now write a simple function in `packages/utils/src/index.ts`:

typescript
export function add(a: number, b: number): number {
  return a + b;
}

export function subtract(a: number, b: number): number {
  return a - b;
}

Install dependencies for this package:

bash
cd ../../
pnpm install

Step 4: Create an App That Uses the Package

Let’s build a quick Next.js app in `apps/web`. Use `create-next-app` with pnpm:

bash
cd apps
pnpm create next-app web --typescript --tailwind --eslint
cd web

Now add `@myrepo/utils` as a dependency:

bash
pnpm add @myrepo/utils

Because we’re using pnpm workspaces, this actually links to `packages/utils` locally. No publishing needed.

Edit `apps/web/app/page.tsx`:

typescript
import { add } from "@myrepo/utils";

export default function Home() {
  return (
    

2 + 3 = {add(2, 3)}

); }

Step 5: Run Tasks with Turborepo

Now the fun part. Run all builds in parallel:

bash
pnpm turbo build

You’ll see output like:


• Packages in scope: @myrepo/utils, web
• Running build in 2 packages
@myrepo/utils:build: cache hit, replaying output
web:build: cache hit, replaying output

The first run will be cold. Subsequent runs are instant if nothing changed. That’s the cache at work.

To run lint across everything:

bash
pnpm turbo lint

Or run tests:

bash
pnpm turbo test

Turborepo automatically parallelizes tasks that don’t depend on each other. You can see the dependency graph with:

bash
pnpm turbo build --graph

Step 6: Add Remote Caching (Optional but Powerful)

Remote caching shares the build cache across your team. No more rebuilding what someone else already built.

  1. Get a Turborepo API token from Vercel (or set up your own server).
  2. Add to `turbo.json`:
json
{
  "remoteCache": {
    "teamId": "team_xxxx",
    "token": "your_token"
  }
}

Or use environment variables. Our team in Vietnam uses this — it’s a game-changer for CI pipelines that used to take 20 minutes and now take 3.

Step 7: Set Up CI/CD with GitHub Actions

Here’s the workflow we use. It caches both pnpm store and Turborepo outputs.

Create `.github/workflows/ci.yml`:

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

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

That’s it. Every PR gets linted, built, and tested in parallel. If the cache is warm, the whole pipeline finishes in under 2 minutes.

Real-World Lessons from Our Team

We migrated a 500K-line monorepo from Lerna to Turborepo last year. Here’s what we learned:

  • Don’t over-split packages. Too many tiny packages create overhead. Aim for 5-10 packages max for a team of 10 developers.
  • Use `outputs` wisely. If you exclude `.next` from outputs, you lose caching for Next.js builds. Include it.
  • pnpm’s strict mode is your friend. It forces you to declare all dependencies explicitly. That prevents subtle bugs in production.

One developer from our Can Tho hub put it best: “I used to wait 15 minutes for a full build. Now I wait 15 seconds.”

When Not to Use a Monorepo

Monorepos aren’t always the answer. If you have:

  • Separate teams with independent release cycles
  • Large binary assets (ML models, videos)
  • Strong security boundaries between services

…then multiple repos might still be better. But for most web app teams, a monorepo with Turborepo and pnpm is the default choice.

Frequently Asked Questions

Q: How is Turborepo different from Nx?

A: Nx is more opinionated and comes with generators, dependency graph visualization, and deeper framework integrations. Turborepo is simpler — it focuses on caching and task orchestration. If you want batteries included, choose Nx. If you want a lightweight, fast pipeline, choose Turborepo.

Q: Can I use npm or Yarn instead of pnpm?

A: Yes, but you’ll lose the strict dependency isolation and disk space savings. Turborepo works with any package manager. That said, pnpm is objectively faster and safer for monorepos. I wouldn’t go back.

Q: How do I handle shared TypeScript configs?

A: Create a `packages/config/` directory. Export `tsconfig.json` files using `”extends”: “@myrepo/config/tsconfig.base.json”`. Then each app/package can extend the base config. Turborepo will cache the config changes automatically.

Q: What about deploying only changed packages?

A: Use `turbo filter` or the `–filter` flag. For example, `pnpm turbo build –filter=web` builds only the web app and its dependencies. In CI, you can use `turbo run build –filter=[origin/main]` to build only packages that changed compared to main.

Related: outsource software development — Learn more about how ECOA AI can help your team.

Related: affordable software outsourcing — Learn more about how ECOA AI can help your team.

Related: affordable software outsourcing — Learn more about how ECOA AI can help your team.

Related reading: Vietnam Outsourcing: Why Smart CTOs Are Moving Their Dev Teams Here in 2025

Leave a Comment

Your email address will not be published. Required fields are marked *

Ready to Build with AI-Powered Developers?

Hire Vietnamese engineers augmented by ECOA AI Platform + Claude Code. 5x faster, 40% cheaper.