Skip to main content

How to Set Up a Monorepo with Turborepo in 2026

·PkgPulse Team

TL;DR

Turborepo + pnpm = the 2026 monorepo standard. create-turbo scaffolds the whole thing in 30 seconds. This guide walks through a real setup: Next.js web app + Hono API + shared packages (UI, database, utils, config). Remote caching makes CI runs 80%+ faster after the first run.

Key Takeaways

  • npx create-turbo@latest — scaffold in 30 seconds
  • pnpm workspaces — package manager layer (packages declare each other as dependencies)
  • turbo.json — defines task dependencies and what to cache
  • Remote cache — CI runs hit cache after the first build, 80%+ time savings
  • workspace:* protocol — pnpm's way to reference internal packages

Scaffold

npx create-turbo@latest my-monorepo
# Prompts: package manager (choose pnpm), app type
cd my-monorepo
pnpm install

Project Structure

my-monorepo/
├── apps/
│   ├── web/           # Next.js 15 frontend
│   └── api/           # Hono API server
├── packages/
│   ├── ui/            # Shared React components
│   ├── database/      # Drizzle schema + db client
│   ├── utils/         # Shared TypeScript utilities
│   └── config/        # Shared tsconfig, biome config
├── turbo.json
├── pnpm-workspace.yaml
└── package.json

pnpm-workspace.yaml

# pnpm-workspace.yaml
packages:
  - 'apps/*'
  - 'packages/*'

turbo.json

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**", ".svelte-kit/**"],
      "cache": true
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": [],
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "cache": true
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": [],
      "cache": true
    },
    "clean": {
      "cache": false
    }
  },
  "remoteCache": {
    "enabled": true
  }
}

packages/config — Shared Configs

// packages/config/package.json
{
  "name": "@myapp/config",
  "version": "0.0.0",
  "private": true,
  "files": ["biome.json", "tsconfig.base.json", "tsconfig.nextjs.json"]
}
// packages/config/tsconfig.base.json
{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "skipLibCheck": true,
    "resolveJsonModule": true
  }
}
// packages/config/tsconfig.nextjs.json
{
  "extends": "./tsconfig.base.json",
  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "bundler",
    "noEmit": true,
    "jsx": "preserve",
    "lib": ["dom", "dom.iterable", "esnext"]
  }
}

packages/ui — Shared Components

// packages/ui/package.json
{
  "name": "@myapp/ui",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": { ".": "./src/index.ts" },
  "devDependencies": {
    "react": "^18.2.0",
    "typescript": "^5.3.0",
    "@myapp/config": "workspace:*"
  },
  "peerDependencies": {
    "react": "^18.2.0"
  }
}
// packages/ui/src/index.ts
export { Button } from './button';
export { Card } from './card';
export type { ButtonProps } from './button';

// packages/ui/src/button.tsx
interface ButtonProps {
  children: React.ReactNode;
  variant?: 'primary' | 'secondary' | 'ghost';
  onClick?: () => void;
}

export function Button({ children, variant = 'primary', onClick }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={`btn btn-${variant}`}
    >
      {children}
    </button>
  );
}

packages/database — Shared DB Client

// packages/database/package.json
{
  "name": "@myapp/database",
  "version": "0.0.0",
  "private": true,
  "main": "./src/index.ts",
  "exports": { ".": "./src/index.ts" },
  "dependencies": {
    "drizzle-orm": "^0.30.0",
    "@neondatabase/serverless": "^0.9.0"
  }
}
// packages/database/src/index.ts
export { db } from './client';
export * from './schema';

// packages/database/src/client.ts
import { drizzle } from 'drizzle-orm/neon-serverless';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';

const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });

apps/web — Next.js App

// apps/web/package.json
{
  "name": "@myapp/web",
  "private": true,
  "dependencies": {
    "@myapp/ui": "workspace:*",
    "@myapp/database": "workspace:*",
    "@myapp/utils": "workspace:*",
    "next": "15.0.0",
    "react": "18.3.0",
    "react-dom": "18.3.0"
  }
}
// apps/web/tsconfig.json
{
  "extends": "@myapp/config/tsconfig.nextjs.json",
  "compilerOptions": {
    "plugins": [{ "name": "next" }],
    "paths": { "@/*": ["./src/*"] }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

Root package.json Scripts

// package.json (root)
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "test": "turbo test",
    "lint": "turbo lint",
    "type-check": "turbo type-check",
    "clean": "turbo clean && rm -rf node_modules"
  }
}

Remote Cache Setup

# Connect to Vercel Remote Cache (free)
npx turbo login
npx turbo link  # Links to your Vercel team

# CI: set TURBO_TOKEN env var
# GitHub Actions example:
env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ vars.TURBO_TEAM }}

# CI result after first run:
# PR #1: full build → 4 minutes (populates cache)
# PR #2: cache hit on unchanged packages → 45 seconds

Compare monorepo tooling on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.