Turborepo Monorepo Guide: Managing Multi-App Projects

Master Turborepo for managing multi-app monorepos. Learn workspace configuration, task pipelines, caching, shared packages, and CI/CD optimization for large-scale projects.

E

ECOSIRE Research and Development Team

ECOSIRE Team

March 5, 20265 min read922 Words

Turborepo Monorepo Guide: Managing Multi-App Projects

Monorepos containing multiple applications and shared packages are now the standard architecture for companies building interconnected software products. Turborepo, acquired by Vercel, provides the build orchestration layer that makes monorepos practical -- handling task dependencies, caching, and parallel execution across dozens of packages.

Key Takeaways

  • Turborepo remote caching can reduce CI build times by 80-90% on incremental changes
  • Task pipelines ensure packages build in the correct dependency order automatically
  • Shared packages (types, validators, utilities) eliminate code duplication across apps
  • pnpm workspaces provide disk-efficient dependency management for monorepos

Monorepo Architecture

Directory Structure

A typical Turborepo monorepo separates applications from shared packages:

my-monorepo/
  apps/
    api/           # NestJS backend
    web/           # Next.js frontend
    docs/          # Documentation site
    mobile/        # React Native app
  packages/
    db/            # Drizzle ORM schemas and migrations
    types/         # Shared TypeScript types
    validators/    # Zod validation schemas
    utils/         # Shared utility functions
    ui/            # Shared UI components
    config/        # Shared configuration (ESLint, TypeScript)
  turbo.json       # Turborepo configuration
  pnpm-workspace.yaml
  package.json     # Root package.json

Workspace Configuration

Define workspaces in pnpm-workspace.yaml:

packages:
  - "apps/*"
  - "packages/*"

Each workspace has its own package.json with dependencies and scripts. Shared packages are referenced using the workspace protocol:

{
  "dependencies": {
    "@myorg/types": "workspace:*",
    "@myorg/validators": "workspace:*",
    "@myorg/utils": "workspace:*"
  }
}

Turborepo Configuration

Task Pipeline

turbo.json defines how tasks relate to each other:

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**", ".next/**"]
    },
    "dev": {
      "dependsOn": ["^build"],
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["build"]
    }
  }
}

Key concepts:

  • dependsOn: ["^build"]: The caret (^) means "build all dependencies first." When building the web app, Turborepo builds packages/types, packages/validators, and packages/utils first.
  • outputs: Files that Turborepo caches. On subsequent runs, if inputs have not changed, Turborepo replays the cached output instead of rebuilding.
  • persistent: true: For dev servers that keep running.
  • cache: false: Disable caching for tasks that must always run (like dev servers).

Shared Packages

Types Package

Centralize TypeScript types used across apps:

// packages/types/src/user.ts
export interface User {
  id: string;
  email: string;
  name: string;
  role: "admin" | "user" | "support";
  createdAt: Date;
}

export type CreateUserDTO = Omit<User, "id" | "createdAt">;
export type UpdateUserDTO = Partial<CreateUserDTO>;

Validators Package

Zod schemas shared between frontend and backend:

// packages/validators/src/user.ts
import { z } from "zod";

export const createUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2).max(255),
  role: z.enum(["admin", "user", "support"]),
});

Build Configuration

Shared packages must be pre-built before apps can consume them. Configure each package with a build script that compiles TypeScript to JavaScript:

{
  "scripts": {
    "build": "tsc --project tsconfig.json",
    "dev": "tsc --watch"
  }
}

Caching Strategy

Local Caching

Turborepo caches task outputs in node_modules/.cache/turbo by default. When you run turbo build and nothing has changed, Turborepo replays the cached output in milliseconds.

Remote Caching

For CI/CD, enable remote caching to share build artifacts across team members and CI runs:

npx turbo login
npx turbo link

Remote caching means a build completed on one developer machine or CI run is available to everyone. The impact is dramatic: incremental CI builds drop from 10 minutes to under 1 minute.


Development Workflow

Running All Apps

pnpm dev       # Starts all apps and watches all packages

Turborepo builds shared packages first (because dev dependsOn ^build), then starts app dev servers in parallel. Changes to shared packages trigger rebuilds that propagate to consuming apps.

Running Specific Apps

pnpm dev --filter=web     # Only the web app and its dependencies
pnpm dev --filter=api     # Only the API and its dependencies

Filtering is essential for large monorepos where starting everything is unnecessary.


CI/CD Optimization

Affected Package Detection

Turborepo determines which packages changed since the last commit and only runs tasks for affected packages:

turbo build --filter=...[HEAD^1]

This runs build only for packages that changed in the latest commit and their dependents.

Parallel Execution

Turborepo runs independent tasks in parallel. If packages/types and packages/utils have no dependency relationship, they build simultaneously. The concurrency level adapts to available CPU cores.


Frequently Asked Questions

Q: How many packages can a Turborepo monorepo handle?

Turborepo handles monorepos with hundreds of packages. Performance scales well because the task graph is analyzed at startup, and caching means most packages skip builds on incremental changes.

Q: Should we use pnpm or npm for workspaces?

pnpm is strongly recommended for monorepos. Its content-addressable store deduplicates dependencies across packages, saving significant disk space and installation time. pnpm workspaces are also more strict about dependency resolution, catching missing dependency declarations.

Q: How do we handle different Node.js versions across apps?

Use engines field in each package.json to specify Node.js version requirements. For runtime differences, configure CI to use the appropriate version per app.

Q: Can we incrementally adopt Turborepo?

Yes. Add turbo.json to an existing pnpm workspace project and configure task pipelines. Existing scripts continue to work. You gain caching and task orchestration immediately without restructuring.


What Is Next

Turborepo transforms monorepo development from a maintenance burden into a productivity multiplier. Start with a simple structure and grow as your project demands.

Contact ECOSIRE for monorepo architecture consulting, or explore our Odoo integration services for multi-app ERP architectures.


Published by ECOSIRE -- helping businesses scale with enterprise software solutions.

E

Written by

ECOSIRE Research and Development Team

Building enterprise-grade digital products at ECOSIRE. Sharing insights on Odoo integrations, e-commerce automation, and AI-powered business solutions.

Chat on WhatsApp