Managing Multiple Apps with Turborepo

By Yusuf Setiyawan
Picture of the author
Published on
Turborepo Monorepo Development

When you're building multiple applications that share code, things can get messy fast. For Ruang Bimbel, I manage 4 separate Next.js apps — and without a proper monorepo setup, it would be a nightmare.

Enter Turborepo.


The Problem: Multiple Apps, Shared Code

Ruang Bimbel isn't just one app. It's actually four:

  1. Landing — Marketing and public-facing pages
  2. User App — Where students learn and take exams
  3. Admin App — Content management for administrators
  4. Super Admin App — System-wide configuration and analytics

These apps share a lot:

  • UI components (buttons, forms, cards)
  • TypeScript types and interfaces
  • API client utilities
  • Authentication logic
  • Styling and design tokens

Without a monorepo, I had two bad options:

Option 1: Copy-paste shared code

  • 🔴 Duplicated code everywhere
  • 🔴 Bug fixes need to be applied 4 times
  • 🔴 Inconsistencies creep in

Option 2: Publish to npm

  • 🔴 Slow development cycle
  • 🔴 Version management headaches
  • 🔴 Overkill for internal packages

A monorepo solves both problems.


Why Turborepo?

I evaluated several monorepo tools:

ToolProsCons
NxFeature-rich, great for large teamsComplex, steep learning curve
LernaBattle-testedSlow, showing its age
pnpm workspacesFast, built into pnpmNo task orchestration
TurborepoFast, simple, Vercel-backedNewer, smaller ecosystem

Turborepo won because it's:

  • Simple — Minimal configuration, just works
  • Fast — Intelligent caching saves tons of time
  • Familiar — Works with your existing package.json scripts

My Monorepo Structure

Here's how my Ruang Bimbel monorepo is organized:

ruangbimbel/
├── apps/
│   ├── landing/          # Marketing site (Next.js)
│   ├── user/             # Student platform (Next.js)
│   ├── admin/            # Admin dashboard (Next.js)
│   └── super-admin/      # System admin (Next.js)
├── packages/
│   ├── ui/               # Shared React components
│   ├── config/           # ESLint, TypeScript configs
│   ├── types/            # Shared TypeScript types
│   └── api-client/       # API utilities
├── turbo.json
├── package.json
└── pnpm-workspace.yaml

Setting Up Turborepo

Getting started is surprisingly simple:

# Create a new turborepo
npx create-turbo@latest

# Or add to existing project
npm install turbo --save-dev

The magic happens in turbo.json:

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

This tells Turbo:

  • build depends on building dependencies first (^build)
  • dev shouldn't be cached (it's a long-running process)
  • lint and test have their own dependency chains

The Power of Caching

Here's where Turborepo shines. Run a build:

$ turbo build

• Packages in scope: @ruangbimbel/ui, admin, landing, user
• Running build in 4 packages
• Remote caching disabled

landing:build: cache hit, replaying output
user:build: cache hit, replaying output  
admin:build: cache miss, executing
super-admin:build: cache hit, replaying output

 Tasks:    4 successful, 4 total
Cached:    3 cached, 4 total
 Time:    12.5s

3 out of 4 builds were cached! Turbo hashed the inputs (source files, dependencies, environment) and realized nothing changed. Instead of rebuilding, it replayed the cached output.

This takes my CI/CD from 8 minutes to under 2 minutes.


Sharing UI Components

The packages/ui folder contains all shared components:

// packages/ui/src/Button.tsx
export interface ButtonProps {
  variant: 'primary' | 'secondary' | 'ghost'
  size: 'sm' | 'md' | 'lg'
  children: React.ReactNode
  onClick?: () => void
}

export function Button({ variant, size, children, onClick }: ButtonProps) {
  return (
    <button 
      className={cn(buttonVariants({ variant, size }))}
      onClick={onClick}
    >
      {children}
    </button>
  )
}

Using it in any app is simple:

// apps/user/src/pages/index.tsx
import { Button } from '@ruangbimbel/ui'

export default function Home() {
  return (
    <Button variant="primary" size="lg">
      Start Learning
    </Button>
  )
}

Change the Button component once, all apps get the update instantly.


Shared TypeScript Types

Type consistency across apps is crucial:

// packages/types/src/exam.ts
export interface Exam {
  id: string
  title: string
  description: string
  duration: number // in minutes
  questions: Question[]
  category: 'CPNS' | 'BUMN' | 'UTBK'
}

export interface Question {
  id: string
  text: string
  options: Option[]
  correctAnswer: string
}

All apps import from the same source:

import type { Exam, Question } from '@ruangbimbel/types'

No more type mismatches between apps!


Running Multiple Apps

Development is a breeze:

# Run all apps in dev mode
turbo dev

# Run specific apps
turbo dev --filter=user --filter=admin

# Run everything except landing
turbo dev --filter=!landing

The --filter flag is incredibly powerful for focusing on what you're working on.


Common Pitfalls I Encountered

1. Forgetting to build packages first

Shared packages need to be built before apps can use them:

{
  "pipeline": {
    "build": {
      "dependsOn": ["^build"]  // This is crucial!
    }
  }
}

2. Hot reload not working for packages

For development, you need to watch package changes:

// packages/ui/package.json
{
  "scripts": {
    "dev": "tsup src/index.ts --watch"
  }
}

3. TypeScript path resolution

Make sure tsconfig.json paths are configured correctly:

{
  "compilerOptions": {
    "paths": {
      "@ruangbimbel/ui": ["../../packages/ui/src"],
      "@ruangbimbel/types": ["../../packages/types/src"]
    }
  }
}

Remote Caching

For teams, Turborepo offers remote caching. Build artifacts are stored in the cloud:

# Login to Vercel (or self-host)
npx turbo login

# Link your repo
npx turbo link

Now when a teammate builds, they get your cached results. A build that takes 8 minutes locally completes in seconds on CI.


My Recommendations

Use a monorepo with Turborepo if:

  • You have multiple apps sharing code
  • You want faster CI/CD pipelines
  • You value simplicity over features
  • Your team is growing

Consider alternatives if:

  • You have a single app
  • You need advanced features like affected-based testing
  • Your team is already comfortable with Nx

Final Thoughts

Turborepo transformed how I manage Ruang Bimbel's codebase. What used to be a mess of copy-pasted code and slow builds is now a clean, organized, and fast development experience.

The initial setup takes an afternoon, but the time saved pays for itself within a week. If you're managing multiple related projects, give Turborepo a try.


Have questions about setting up your own monorepo? Feel free to reach out!