.agents/skills/turborepo/references/best-practices/structure.md
Detailed guidance on structuring a Turborepo monorepo.
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
// package.json
{
"workspaces": ["apps/*", "packages/*"]
}
{
"name": "my-monorepo",
"private": true,
"packageManager": "[email protected]",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"lint": "turbo run lint",
"test": "turbo run test"
},
"devDependencies": {
"turbo": "latest"
}
}
Key points:
private: true - Prevents accidental publishingpackageManager - Enforces consistent package manager versionturbo run - No actual build logic here!Always use package tasks. Only use Root Tasks if you cannot succeed with package tasks.
// packages/web/package.json
{
"scripts": {
"build": "next build",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}
// packages/api/package.json
{
"scripts": {
"build": "tsc",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit"
}
}
Package tasks enable Turborepo to:
web#lint and api#lint simultaneouslyturbo run test --filter=web for just one packageRoot Tasks are a fallback for tasks that truly cannot run per-package:
// AVOID unless necessary - sequential, not parallelized, can't filter
{
"scripts": {
"lint": "eslint apps/web && eslint apps/api && eslint packages/ui"
}
}
{
"$schema": "https://v2-8-21-canary-9.turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
With futureFlags.globalConfiguration, global settings move under a global key:
{
"$schema": "https://v2-8-21-canary-9.turborepo.dev/schema.json",
"futureFlags": { "globalConfiguration": true },
"global": {
"inputs": ["tsconfig.json"],
"env": ["CI"]
},
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"lint": {},
"test": {
"dependsOn": ["build"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
You can group packages by adding more workspace paths:
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
- "packages/config/*" # Grouped configs
- "packages/features/*" # Feature packages
This allows:
packages/
├── ui/
├── utils/
├── config/
│ ├── eslint/
│ ├── typescript/
│ └── tailwind/
└── features/
├── auth/
└── payments/
# BAD: Nested wildcards cause ambiguous behavior
packages:
- "packages/**" # Don't do this!
packages/ui/
├── package.json # Required: Makes it a package
├── src/ # Source code
│ └── button.tsx
└── tsconfig.json # TypeScript config (if using TS)
{
"name": "@repo/ui", // Unique, namespaced name
"version": "0.0.0", // Version (can be 0.0.0 for internal)
"private": true, // Prevents accidental publishing
"exports": {
// Entry points
"./button": "./src/button.tsx"
}
}
Create a shared TypeScript config package:
packages/
└── typescript-config/
├── package.json
├── base.json
├── nextjs.json
└── library.json
// packages/typescript-config/base.json
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022"
}
}
// packages/ui/tsconfig.json
{
"extends": "@repo/typescript-config/library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
You likely don't need a tsconfig.json in the workspace root. Each package should have its own config extending from the shared config package.
packages/
└── eslint-config/
├── package.json
├── base.js
├── next.js
└── library.js
// packages/eslint-config/package.json
{
"name": "@repo/eslint-config",
"exports": {
"./base": "./base.js",
"./next": "./next.js",
"./library": "./library.js"
}
}
// apps/web/.eslintrc.js
module.exports = {
extends: ["@repo/eslint-config/next"]
};
A lockfile is required for:
Without a lockfile, you'll see unpredictable behavior.