6.5 KiB
Turborepo Monorepo Best Practices — MeshiTrack
Instruction file for managing the monorepo workspace.
Workspace Structure
MeshiTrack/
├── packages/
│ ├── api/ # NestJS backend → @meshitrack/api
│ ├── web/ # Next.js frontend → @meshitrack/web
│ └── shared/ # Shared types & DTOs → @meshitrack/shared
├── docker/ # Docker configs (not a package)
├── docs/ # Documentation (not a package)
├── turbo.json # Turborepo pipeline config
├── package.json # Root workspace config
├── tsconfig.base.json
├── .eslintrc.js
├── .prettierrc
└── .gitignore
Root package.json
{
"name": "meshitrack",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"lint": "turbo run lint",
"test": "turbo run test",
"typecheck": "turbo run typecheck",
"clean": "turbo run clean"
},
"devDependencies": {
"turbo": "^2.x",
"typescript": "^5.x",
"eslint": "^9.x",
"prettier": "^3.x"
}
}
turbo.json Configuration
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**", "!.next/cache/**"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^build"]
},
"test": {
"dependsOn": ["^build"]
},
"typecheck": {
"dependsOn": ["^build"]
},
"clean": {
"cache": false
}
}
}
Key concepts
dependsOn: ["^build"]: Before running a task in a package, first build all its dependencies. This ensuressharedis built beforeapiorwebrun.outputs: What Turborepo caches. If outputs haven't changed, Turborepo replays from cache.persistent: true: For long-running dev servers that shouldn't be cached.cache: false: Disables caching for tasks that should always run.
Package Dependencies
Shared package is the foundation
@meshitrack/shared ← @meshitrack/api
← @meshitrack/web
Both api and web depend on shared, but never on each other.
Reference shared in package.json
// packages/api/package.json
{
"name": "@meshitrack/api",
"dependencies": {
"@meshitrack/shared": "workspace:*"
}
}
// packages/web/package.json
{
"name": "@meshitrack/web",
"dependencies": {
"@meshitrack/shared": "workspace:*"
}
}
Import from shared
// In packages/api or packages/web
import { Product, NutritionInfo, ProductCategory } from '@meshitrack/shared';
import { CreateProductSchema } from '@meshitrack/shared/validation';
Shared Package Setup
packages/shared/package.json
{
"name": "@meshitrack/shared",
"version": "0.0.1",
"private": true,
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./validation": {
"types": "./dist/validation/index.d.ts",
"default": "./dist/validation/index.js"
}
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch",
"clean": "rm -rf dist",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.x"
},
"devDependencies": {
"typescript": "^5.x"
}
}
packages/shared/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"]
}
Shared package must be platform-agnostic
Rules for code in packages/shared:
- No Node.js imports (
fs,path,http, etc.) - No NestJS decorators or imports
- No Next.js imports
- No DOM/browser APIs
- Only pure TypeScript: types, interfaces, enums, Zod schemas, utility functions
TypeScript Configuration
Root tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"incremental": true
},
"exclude": ["node_modules", "dist"]
}
Each package extends this and overrides as needed (e.g., web uses "jsx": "preserve", "module": "esnext").
Development Workflow
Start all packages in dev mode
npm run dev
# Turborepo runs: shared (build) → then api (dev) + web (dev) in parallel
Build all packages
npm run build
# Turborepo builds shared first, then api and web in parallel
Run tasks for a specific package
npx turbo run dev --filter=@meshitrack/api
npx turbo run test --filter=@meshitrack/web
Add a dependency to a specific package
cd packages/api
npm install @nestjs/schedule
# Or from root:
npm install @nestjs/schedule --workspace=packages/api
Caching
Turborepo remote caching (optional)
For CI, consider enabling remote caching to share build cache across machines:
npx turbo login
npx turbo link
Or use a self-hosted cache server for full self-hosted setup.
Local caching
Turborepo caches locally in node_modules/.cache/turbo by default. It's fast and requires no setup.
CI Integration
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npx turbo run build lint test typecheck
Common Pitfalls
-
Forgetting to build shared: If
apiorwebcan't find types fromshared, runnpm run buildfrom root. ThedependsOn: ["^build"]config handles this in Turborepo tasks. -
Circular dependencies: Never import from
apiinwebor vice versa. Only import fromshared. -
Version mismatches: Keep TypeScript versions aligned across all packages. Pin in root
devDependencies. -
Large
node_modules: Usenpmworkspaces hoisting. Most dependencies are installed at root level. Only package-specific versions go in package-levelnode_modules.