// 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// DevOps
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 readOur 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.
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:
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.json1# pnpm-workspace.yaml2packages:3 - "apps/*"4 - "packages/*"Apps depend on packages through the workspace protocol, so they always use the local version:
1{2 "dependencies": {3 "@acme/types": "workspace:*",4 "@acme/ui": "workspace:*"5 }6}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.
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:
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:
1pnpm turbo run lint test build --filter="...[origin/main]"The danger of a monorepo is that everything can import everything. Keep boundaries explicit:
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.
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.