Loading introduction
Loading stats
Loading about
Loading posts
Loading article
Skip to content
Md Nayeem Hossain
Explorer
home.tsx
monorepos-pnpm-workspaces-turborepo.md
monorepos-pnpm-workspaces-turborepo.md
blog.json

// DevOps

Monorepos with pnpm Workspaces and Turborepo

One repository for web, admin and API: pnpm workspaces for strict dependencies and Turborepo caching so only what changed gets built.

Md Nayeem HossainAug 24, 20263 min read
šŸš€

Monorepos with pnpm Workspaces and Turborepo#

Our setup used to be four repositories: the web app, the admin panel, the API and a shared package of types and utilities. Every change to a shared type meant publishing a new package version, bumping it in three places, and hoping nobody was on an old version. Half our "bugs" were version mismatches.

Moving to a monorepo removed that entire category of problem. It also introduced new ones, which the right tooling solves.

What a monorepo is (and is not)#

A monorepo is a single repository containing multiple projects with clear boundaries. It is not one giant application, and it does not mean everything deploys together. Each app still builds and deploys independently.

The benefits:

  • Atomic changes. Update an API response type and every consumer in one pull request.
  • One source of truth for shared code, with no publishing step.
  • Consistent tooling: one lint config, one TypeScript base config, one CI setup.
  • Easier refactoring across project boundaries.

A practical structure#

text
1.2ā”œā”€ā”€ apps/3│   ā”œā”€ā”€ web/          # Next.js public site4│   ā”œā”€ā”€ admin/        # admin dashboard5│   └── api/          # NestJS backend6ā”œā”€ā”€ packages/7│   ā”œā”€ā”€ types/        # shared DTOs and domain types8│   ā”œā”€ā”€ ui/           # shared React components9│   └── config/       # eslint, tsconfig, tailwind presets10ā”œā”€ā”€ pnpm-workspace.yaml11└── turbo.json
yaml
1# pnpm-workspace.yaml2packages:3  - "apps/*"4  - "packages/*"

Apps depend on packages through the workspace protocol, so they always use the local version:

json
1{2  "dependencies": {3    "@acme/types": "workspace:*",4    "@acme/ui": "workspace:*"5  }6}

Why pnpm#

pnpm installs each package version once in a global store and links it, which makes installs fast and disk-efficient. More importantly, it is strict: a project can only import dependencies it actually declares. In a monorepo, that prevents the classic bug where an app works only because a sibling project happened to install a package.

Turborepo: only build what changed#

As the repository grows, running every build and test on every commit becomes slow. Turborepo understands the dependency graph between workspaces and caches task outputs:

json
1{2  "$schema": "https://turbo.build/schema.json",3  "tasks": {4    "build": {5      "dependsOn": ["^build"],6      "outputs": ["dist/**", ".next/**", "!.next/cache/**"]7    },8    "lint": {},9    "test": {10      "dependsOn": ["^build"]11    },12    "dev": {13      "cache": false,14      "persistent": true15    }16  }17}

"^build" means "build my dependencies first". If nothing in packages/types or apps/api changed, turbo run build replays the cached output in milliseconds instead of rebuilding. With remote caching enabled, CI and teammates share that cache, and an unchanged app is never rebuilt anywhere.

In CI, combine it with filtering so a pull request only checks what it affects:

bash
1pnpm turbo run lint test build --filter="...[origin/main]"

Sharing code without coupling everything#

The danger of a monorepo is that everything can import everything. Keep boundaries explicit:

  • Packages expose a public entry point; apps never import from another package's internal folders.
  • Apps never import from other apps. If two apps need the same code, it becomes a package.
  • Shared packages should be stable and generic. A "shared" package that changes with every feature is really part of one app.
  • Enforce rules with lint configuration so violations fail CI.

Shared types between frontend and backend#

The biggest single win for us was sharing API types. When the backend changes a response shape, the frontend fails to compile in the same pull request instead of failing at runtime in production. Generating types from an OpenAPI spec or using a schema library such as Zod for both validation and types makes this even stronger.

Things to plan for#

  • CI time: without caching and filtering, CI gets slower with every project. Set up Turborepo caching early.
  • Ownership: use a CODEOWNERS file so the right team reviews changes to their area.
  • Deploys: each app needs its own deploy pipeline, triggered only when that app or its dependencies change.
  • Dependency upgrades: upgrading a framework now affects several apps at once. Treat it as a coordinated change.

Conclusion#

A monorepo with pnpm workspaces and Turborepo gives you atomic changes across apps, shared types without publishing, and fast builds thanks to dependency-aware caching. Keep the boundaries between apps and packages explicit, cache aggressively in CI, and give each app its own deploy pipeline. For a team maintaining several connected applications, it removes far more friction than it adds.

#Monorepo#pnpm#Turborepo#TypeScript
OlderTechnical Debt: How to Measure It and Actually Pay It DownNewer Email Deliverability for Developers: SPF, DKIM, DMARC and Beyond

Outline

  • Monorepos with pnpm Workspaces and Turborepo
  • What a monorepo is (and is not)
  • A practical structure
  • Why pnpm
  • Turborepo: only build what changed
  • Sharing code without coupling everything
  • Shared types between frontend and backend
  • Things to plan for
  • Conclusion