MeshiTrack/docs/instructions/turborepo.md

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 ensures shared is built before api or web run.
  • 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

  1. Forgetting to build shared: If api or web can't find types from shared, run npm run build from root. The dependsOn: ["^build"] config handles this in Turborepo tasks.

  2. Circular dependencies: Never import from api in web or vice versa. Only import from shared.

  3. Version mismatches: Keep TypeScript versions aligned across all packages. Pin in root devDependencies.

  4. Large node_modules: Use npm workspaces hoisting. Most dependencies are installed at root level. Only package-specific versions go in package-level node_modules.