Scaffold a production monorepo: Nx vs Turborepo decision, workspace structure, shared packages, CI matrix builds, versioning strategy (fixed vs. independent), and dependency graph visualization.
"A monorepo is an organizational decision before it's a tooling decision — get the ownership model right first."
Five-step procedure to scaffold a monorepo with correct workspace tooling, shared package conventions, CI matrix strategy, and version management — with an explicit Nx vs Turborepo decision framework based on team size and ecosystem fit.
| Criterion | Choose Nx | Choose Turborepo |
|---|---|---|
| Codebase size | Large (20+ packages) | Any size |
| Code generation needs | Yes (generators) | No |
| Angular or Nest.js present | Yes | No |
| Distributed task execution | Yes (Nx Cloud) | Yes (Vercel Remote Cache) |
| Learning curve tolerance | Medium | Low |
| Plugin ecosystem depth | High | Growing |
| Full repo visualization | Yes (graph command) | Limited |
| Independent version per package | Yes | Yes (via Changesets) |
Lean toward Nx when: multiple app types, need generators, enterprise scale. Lean toward Turborepo when: mostly JS/TS, simpler mental model preferred, Vercel hosting.
my-monorepo/
├── apps/
│ ├── web/ # Next.js / React app
│ ├── api/ # Express / Fastify / NestJS API
│ └── mobile/ # React Native / Expo
├── packages/
│ ├── ui/ # Shared component library
│ ├── types/ # Shared TypeScript types (no runtime)
│ ├── utils/ # Shared utility functions
│ ├── config/ # Shared configs (ESLint, tsconfig, tailwind)
│ └── database/ # Prisma schema + generated client
├── tools/
│ └── scripts/ # Repo-wide automation scripts
├── .changeset/ # Changesets for versioning
├── pnpm-workspace.yaml
├── package.json # Root — devDependencies only
├── turbo.json OR nx.json
└── tsconfig.base.jsonpnpm-workspace.yamlpackages:
- 'apps/*'
- 'packages/*'
- 'tools/*'package.json{
"name": "my-monorepo",
"private": true,
"engines": { "node": ">=20", "pnpm": ">=9" },
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"test": "turbo test",
"lint": "turbo lint",
"typecheck": "turbo typecheck",
"clean": "turbo clean && rm -rf node_modules"
},
"devDependencies": {
"turbo": "^2.0.0",
"@changesets/cli": "^2.27.0",
"typescript": "^5.4.0"
},
"packageManager": "pnpm@9.0.0"
}packages/types — Zero-runtime shared types// packages/types/src/index.ts
export interface User {
id: string;
email: string;
createdAt: Date;
}
export interface ApiResponse<T> {
data: T;
error?: string;
meta?: { page: number; total: number };
}// packages/types/package.json
{
"name": "@myrepo/types",
"version": "0.0.1",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": { "import": "./src/index.ts" }
},
"scripts": {
"typecheck": "tsc --noEmit"
}
}tsconfig.base.json{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"paths": {
"@myrepo/types": ["./packages/types/src/index.ts"],
"@myrepo/utils": ["./packages/utils/src/index.ts"],
"@myrepo/ui": ["./packages/ui/src/index.ts"]
}
}
}turbo.json — Pipeline Configuration{
"$schema": "https://turbo.build/schema.json",
"globalEnv": ["NODE_ENV", "DATABASE_URL"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "!dist/cache/**"],
"env": ["NEXT_PUBLIC_*"]
},
"dev": {
"cache": false,
"persistent": true
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"],
"env": ["DATABASE_URL_TEST"]
},
"lint": { "outputs": [] },
"typecheck": { "dependsOn": ["^typecheck"], "outputs": [] },
"clean": { "cache": false }
}
}# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 2 }
- uses: pnpm/action-setup@v4
with: { version: 9 }
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Restore Turbo Cache
uses: actions/cache@v4
with:
path: .turbo
key: ${{ runner.os }}-turbo-${{ github.sha }}
restore-keys: |
${{ runner.os }}-turbo-
- run: pnpm turbo build test lint typecheck
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}pnpm add -D @changesets/cli -w
pnpm changeset init// .changeset/config.json
{
"changelog": "@changesets/cli/changelog",
"commit": false,
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@myrepo/config"]
}# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile
- uses: changesets/action@v1
with:
publish: pnpm changeset publish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}^build dependency in turbo — Packages build in dependency order, never stale outputs.packageManager field in root package.json — Enforces consistent package manager version.--frozen-lockfile in CI — Prevents silent lockfile mutations in CI.tsconfig.base.json — All packages inherit base config; no per-package duplication.private: true in package root — Prevents accidental publish of root workspace.packages/*, never from other apps.packages/ui importing from packages/database while packages/database imports types from packages/ui deadlocks the build graph.workspace:* version without resolution — Publishing packages with unresolved workspace:* versions breaks consumers outside the monorepo.packages/ as a publishable package — Config-only packages (ESLint config, tsconfig) belong in packages/config, not as publishable packages.outputs in turbo.json — Turbo doesn't know what to cache; every task re-runs on cache hit.apps/web importing from apps/api creates tight coupling; share through packages/ instead..npmrc.2699c1e
Also appears in
since Aug 28, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.