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.
See the live comparison
View turborepo vs. nx on PkgPulse →