Managing Multiple Apps with Turborepo

- Published on

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:
- Landing — Marketing and public-facing pages
- User App — Where students learn and take exams
- Admin App — Content management for administrators
- 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:
| Tool | Pros | Cons |
|---|---|---|
| Nx | Feature-rich, great for large teams | Complex, steep learning curve |
| Lerna | Battle-tested | Slow, showing its age |
| pnpm workspaces | Fast, built into pnpm | No task orchestration |
| Turborepo | Fast, simple, Vercel-backed | Newer, 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!