Kebanyakan tutorial monorepo ditulis untuk tim 20 orang dengan DevOps dedicated. Kamu bukan itu. Kamu solo engineer yang punya 3-4 proyek saling berkaitan dan bosan copy-paste shared utilities antar repo.
Itu masalah nyata. Setiap kali auth-utils di-update, kamu harus ingat repo mana saja yang perlu di-sync. Lupa satu? Production error jam 2 pagi. Menyenangkan sekali.
Post ini adalah monorepo development setup guide yang saya pakai sendiri — bukan teori, tapi konfigurasi aktual yang jalan di mesin saya (Ubuntu 22.04, Node 20.x) dan di VPS Hetzner CX21 yang harganya €5.77/bulan. Kita pakai pnpm workspaces + Turborepo 2.x karena kombinasi ini paling masuk akal untuk skala solo hingga tim kecil.
Kenapa Monorepo, dan Kenapa Bukan Polyrepo?
Polyrepo terasa lebih simpel di awal. Setiap proyek independen, Git history bersih, deployment terpisah. Tapi begitu kamu punya shared code — design system, tipe TypeScript, helper functions — polyrepo mulai menyiksa.
Monorepo bukan berarti satu deployment. Itu kesalahpahaman paling umum. Monorepo = satu repository, banyak package, deployment tetap bisa terpisah. Kamu tetap bisa deploy apps/api ke VPS dan apps/web ke Vercel.
Alternatif yang sering disebut:
| Tool | Cocok untuk | Kelemahan |
|---|---|---|
| Nx | Tim besar, enterprise | Konfigurasi verbose, overkill untuk solo |
| Lerna (v7+) | Package library publik | Kurang optimal untuk apps |
| Turborepo | Apps + packages, solo/tim kecil | Dokumentasi kadang ketinggalan versi |
| Bazel | Skala Google | Kurva belajar sangat curam |
Saya pilih Turborepo karena setup awal bisa selesai dalam 30 menit dan caching-nya bekerja tanpa konfigurasi rumit.
Struktur Folder yang Tidak Akan Kamu Sesali
Jangan ikuti struktur default dari create-turbo mentah-mentah. Ini yang saya pakai:
my-monorepo/
├── apps/
│ ├── web/ # Next.js frontend
│ ├── api/ # Fastify/Express backend
│ └── docs/ # Dokumentasi internal (optional)
├── packages/
│ ├── ui/ # Shared React components
│ ├── config/ # ESLint, TypeScript configs
│ ├── types/ # Shared TypeScript types
│ └── utils/ # Pure utility functions
├── turbo.json
├── package.json # Root package.json
└── pnpm-workspace.yaml
Aturan sederhananya: apps/ berisi sesuatu yang di-deploy. packages/ berisi sesuatu yang di-import oleh apps lain. Jangan campur keduanya.
Setup Awal: Step by Step
1. Inisialisasi pnpm workspace
mkdir my-monorepo && cd my-monorepo
pnpm init
Buat file pnpm-workspace.yaml:
packages:
- 'apps/*'
- 'packages/*'
Edit package.json root:
{
"name": "my-monorepo",
"private": true,
"scripts": {
"build": "turbo build",
"dev": "turbo dev",
"lint": "turbo lint",
"test": "turbo test"
},
"devDependencies": {
"turbo": "^2.1.0"
},
"engines": {
"node": ">=20.0.0",
"pnpm": ">=9.0.0"
}
}
2. Konfigurasi Turborepo
Buat turbo.json di root:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**", "build/**"]
},
"dev": {
"cache": false,
"persistent": true
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["^build"],
"outputs": ["coverage/**"]
}
}
}
Tanda ^ di "^build" artinya: jalankan build di semua dependencies dulu sebelum build package ini. Ini yang bikin Turborepo tahu urutan eksekusi yang benar.
3. Buat Shared Package Pertama
Kita buat packages/utils sebagai contoh:
mkdir -p packages/utils/src
packages/utils/package.json:
{
"name": "@myapp/utils",
"version": "0.1.0",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
}
}
Perhatikan: saya export langsung file .ts, bukan hasil compile. Ini trik yang saya pelajari dari setup Turborepo 2.x — selama consumer package bisa handle TypeScript (Next.js dan Vite bisa), tidak perlu build step tambahan untuk internal packages. Lebih cepat, lebih sedikit konfigurasi.
packages/utils/src/index.ts:
export function formatDate(date: Date): string {
return new Intl.DateTimeFormat('id-ID', {
day: 'numeric',
month: 'long',
year: 'numeric',
}).format(date);
}
export function slugify(text: string): string {
return text
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
}
4. Pakai Shared Package di App
Di apps/web, tambahkan dependency:
pnpm --filter web add @myapp/utils@workspace:*
Syntax workspace:* artinya pakai versi lokal dari workspace, bukan dari npm registry. Ini penting.
Sekarang di kode Next.js kamu:
import { formatDate, slugify } from '@myapp/utils';
export default function BlogPost({ title, publishedAt }: Props) {
return (
<article>
<h1>{title}</h1>
<time>{formatDate(new Date(publishedAt))}</time>
</article>
);
}
Perubahan di packages/utils langsung terlihat di apps/web tanpa rebuild. Hot reload bekerja cross-package.
TypeScript Config yang Tidak Bikin Pusing
Ini bagian yang sering diabaikan tapi krusial. Buat packages/config/tsconfig/ dengan beberapa base config:
packages/config/tsconfig/base.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"strict": true,
"skipLibCheck": true,
"noEmit": true,
"esModuleInterop": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true
}
}
packages/config/tsconfig/nextjs.json:
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "./base.json",
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"jsx": "preserve",
"plugins": [{"name": "next"}]
}
}
Di apps/web/tsconfig.json:
{
"extends": "@myapp/config/tsconfig/nextjs.json",
"include": [".", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Satu sumber kebenaran untuk TypeScript config. Update di satu tempat, berlaku di semua apps.
Turborepo Remote Caching: Gratis Sampai Batas Wajar
Ini fitur yang sering diabaikan solo engineer. Turborepo bisa cache hasil build ke remote storage, jadi CI tidak perlu rebuild dari nol setiap kali.
Opsi gratis: Vercel Remote Cache (gratis untuk hobby plan). Opsi self-hosted: ducktape atau turborepo-remote-cache yang bisa jalan di VPS sendiri.
Setup self-hosted remote cache di VPS:
# Di VPS
npx turborepo-remote-cache
# Default port: 3000
# Set environment variable TURBO_TOKEN dan TURBO_TEAM
Di .env lokal dan CI:
TURBO_TOKEN=your-secret-token
TURBO_TEAM=your-team-name
TURBO_REMOTE_CACHE_SIGNATURE_KEY=another-secret
Di turbo.json, tambahkan:
{
"remoteCache": {
"enabled": true
}
}
Dengan remote cache aktif, CI pipeline yang biasanya 8 menit bisa turun ke 90 detik kalau tidak ada perubahan di packages yang di-build.
CI dengan GitHub Actions: Minimal tapi Efektif
Saya tidak pakai setup CI yang fancy. Ini yang jalan:
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build, lint, test
run: pnpm turbo build lint test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
Satu job, tiga task, Turborepo yang atur parallelisme dan caching. GitHub Actions free tier (2000 menit/bulan) cukup untuk solo engineer dengan aktivitas normal.
Pitfalls yang Pernah Saya Kena
Circular dependencies. packages/ui import dari packages/utils, lalu packages/utils import dari packages/ui. Turborepo akan error dengan pesan yang tidak terlalu helpful. Solusinya: gambar dependency graph sebelum mulai coding. packages/ tidak boleh saling import secara circular.
node_modules hoisting yang aneh. pnpm punya behavior hoisting yang berbeda dari npm/yarn. Kalau ada package yang complain tidak ketemu dependency, coba tambahkan di package.json root atau set shamefullyHoist: true di .npmrc (tapi ini last resort).
Dev server port conflict. Kalau turbo dev jalankan semua apps sekaligus, pastikan setiap app pakai port berbeda. Saya set di masing-masing .env.local: apps/web di 3000, apps/api di 3001, apps/docs di 3002.
Cache stale setelah rename file. Turborepo cache berbasis hash dari file content dan dependency. Kalau rename file tanpa ubah content, cache mungkin tidak invalidate dengan benar. turbo build --force untuk bypass cache kalau curiga ada masalah.
Kalau kamu baru mulai dengan self-hosted setup, baca juga artikel saya tentang cara setup VPS untuk development environment — banyak hal yang overlap dengan monorepo workflow ini.
Untuk manajemen secrets antar apps dalam monorepo, this guide on managing env variables in monorepo lebih detail soal .env cascading.
Kapan Monorepo Bukan Jawaban yang Tepat
Jujur: kalau proyekmu benar-benar independen dan tidak ada shared code, monorepo hanya menambah overhead. Setup ini paling masuk akal kalau:
- Kamu punya minimal 2 apps yang share kode (types, utils, components)
- Kamu ingin atomic commits — satu commit bisa update shared package dan semua consumer-nya sekaligus
- Kamu lelah mengelola versioning antar repo
Kalau kamu cuma punya satu app dengan satu backend, tetap di polyrepo. Jangan over-engineer.
Kesimpulan
Monorepo development setup guide ini bukan satu-satunya cara, tapi ini yang paling sedikit friction untuk solo engineer berdasarkan pengalaman saya. pnpm workspaces handle dependency isolation dengan baik, Turborepo handle build orchestration dan caching, GitHub Actions free tier cukup untuk CI.
Besok, coba satu langkah konkret: ambil dua repo yang saling share utility functions, buat monorepo baru dengan struktur di atas, dan pindahkan shared code ke packages/utils. Tidak perlu migrasi semua sekaligus. Mulai dari shared code yang paling sering di-copy-paste — itu titik sakit terbesar yang langsung terasa manfaatnya.
Kalau ada pertanyaan soal setup spesifik, tinggalkan komentar. Saya baca semuanya.