Skip to main content
Back to Blog
Tutorials
3 min read
November 22, 2024

How to Create a Monorepo with Turborepo

Set up a monorepo with Turborepo for shared packages, multiple apps, and efficient build caching.

Ryel Banfield

Founder & Lead Developer

Monorepos let you manage multiple apps and shared packages in one repository. Turborepo makes this fast with build caching.

Step 1: Create the Monorepo

npx create-turbo@latest my-monorepo
cd my-monorepo

Or set up manually:

mkdir my-monorepo && cd my-monorepo
pnpm init

Step 2: Workspace Structure

my-monorepo/
├── apps/
│   ├── web/          # Main Next.js app
│   ├── docs/         # Documentation site
│   └── admin/        # Admin dashboard
├── packages/
│   ├── ui/           # Shared UI components
│   ├── config/       # Shared configs (ESLint, TypeScript)
│   ├── database/     # Database schema and client
│   └── utils/        # Shared utilities
├── turbo.json
├── package.json
└── pnpm-workspace.yaml

Step 3: Configure pnpm Workspaces

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

Step 4: Root package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "build": "turbo run build",
    "dev": "turbo run dev",
    "lint": "turbo run lint",
    "test": "turbo run test",
    "format": "prettier --write \"**/*.{ts,tsx,md}\""
  },
  "devDependencies": {
    "prettier": "^3.4.0",
    "turbo": "^2.3.0",
    "typescript": "^5.7.0"
  },
  "packageManager": "pnpm@9.15.0"
}

Step 5: turbo.json Configuration

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "!.next/cache/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "lint": {
      "dependsOn": ["^build"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "db:generate": {
      "cache": false
    },
    "db:migrate": {
      "cache": false
    }
  }
}

Step 6: Shared UI Package

// packages/ui/package.json
{
  "name": "@repo/ui",
  "version": "0.0.0",
  "private": true,
  "exports": {
    "./button": "./src/button.tsx",
    "./card": "./src/card.tsx",
    "./input": "./src/input.tsx",
    "./badge": "./src/badge.tsx",
    "./globals.css": "./src/globals.css"
  },
  "devDependencies": {
    "@repo/config": "workspace:*",
    "typescript": "^5.7.0"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}
// packages/ui/src/button.tsx
import { forwardRef } from "react";

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: "primary" | "secondary" | "outline" | "ghost";
  size?: "sm" | "md" | "lg";
}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = "primary", size = "md", className, children, ...props }, ref) => {
    const variants = {
      primary: "bg-blue-600 text-white hover:bg-blue-700",
      secondary: "bg-gray-100 text-gray-900 hover:bg-gray-200",
      outline: "border border-gray-300 hover:bg-gray-50",
      ghost: "hover:bg-gray-100",
    };

    const sizes = {
      sm: "px-3 py-1.5 text-sm",
      md: "px-4 py-2 text-sm",
      lg: "px-6 py-3 text-base",
    };

    return (
      <button
        ref={ref}
        className={`inline-flex items-center justify-center rounded-lg font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:opacity-50 ${variants[variant]} ${sizes[size]} ${className ?? ""}`}
        {...props}
      >
        {children}
      </button>
    );
  }
);

Button.displayName = "Button";

Step 7: Shared Database Package

// packages/database/package.json
{
  "name": "@repo/database",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts",
    "./schema": "./src/schema/index.ts"
  },
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio"
  },
  "dependencies": {
    "drizzle-orm": "^0.38.0",
    "@neondatabase/serverless": "^0.10.0"
  },
  "devDependencies": {
    "drizzle-kit": "^0.30.0"
  }
}
// packages/database/src/index.ts
import { drizzle } from "drizzle-orm/neon-serverless";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";

export function createDb(databaseUrl: string) {
  const sql = neon(databaseUrl);
  return drizzle({ client: sql, schema });
}

export type Database = ReturnType<typeof createDb>;
export * from "./schema";

Step 8: Shared Utils Package

// packages/utils/package.json
{
  "name": "@repo/utils",
  "version": "0.0.0",
  "private": true,
  "exports": {
    ".": "./src/index.ts"
  }
}
// packages/utils/src/index.ts
export function formatCurrency(amount: number, currency: string = "USD"): string {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}

export function slugify(text: string): string {
  return text
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/(^-|-$)/g, "");
}

export function truncate(text: string, maxLength: number): string {
  if (text.length <= maxLength) return text;
  return text.slice(0, maxLength).trimEnd() + "...";
}

export function cn(...classes: (string | undefined | null | false)[]): string {
  return classes.filter(Boolean).join(" ");
}

Step 9: App Configuration

// apps/web/package.json
{
  "name": "@repo/web",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build",
    "lint": "next lint"
  },
  "dependencies": {
    "@repo/ui": "workspace:*",
    "@repo/database": "workspace:*",
    "@repo/utils": "workspace:*",
    "next": "^15.0.0",
    "react": "^19.0.0",
    "react-dom": "^19.0.0"
  }
}

Step 10: Use Shared Packages

// apps/web/app/page.tsx
import { Button } from "@repo/ui/button";
import { Card } from "@repo/ui/card";
import { formatCurrency, truncate } from "@repo/utils";

export default function HomePage() {
  return (
    <main>
      <h1>Welcome</h1>
      <p>Price: {formatCurrency(29.99)}</p>
      <p>{truncate("This is a long description...", 20)}</p>
      <Button variant="primary">Get Started</Button>
    </main>
  );
}

Step 11: Shared TypeScript Config

// packages/config/tsconfig/base.json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "moduleResolution": "bundler",
    "module": "esnext",
    "target": "es2022",
    "lib": ["es2022", "dom", "dom.iterable"],
    "jsx": "react-jsx",
    "isolatedModules": true,
    "incremental": true
  },
  "exclude": ["node_modules"]
}

Step 12: Development Workflow

# Start all apps in dev mode
pnpm dev

# Start specific app
pnpm --filter @repo/web dev

# Build all packages
pnpm build

# Add dependency to a specific package
pnpm --filter @repo/web add lodash

# Lint everything
pnpm lint

Summary

  • Turborepo caches and parallelizes builds across packages
  • Shared packages eliminate code duplication
  • workspace:* links packages without publishing
  • Each app can be deployed independently
  • Single source of truth for configs and types

Need Monorepo Architecture?

We design and implement scalable monorepo architectures for growing engineering teams. Contact us to discuss your project.

monorepoTurborepoworkspacepnpmNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles