Membangun Monorepo Efisien dengan Workspaces: Struktur, Dependensi, dan Otomatisasi (Tanpa Framework Berat)
1. Pendahuluan
Sebagai developer, kita sering kali dihadapkan pada tantangan mengelola banyak proyek yang saling terkait. Mungkin ada aplikasi frontend, backend API, library UI bersama, atau utilitas yang dipakai di berbagai tempat. Mengelola semuanya dalam repositori terpisah bisa jadi mimpi buruk: duplikasi kode, versi dependensi yang tidak konsisten, dan proses deployment yang rumit.
Di sinilah monorepo hadir sebagai solusi yang elegan. Monorepo adalah strategi di mana kita menyimpan banyak proyek kode dalam satu repositori Git yang besar. Ini bukan konsep baru, perusahaan teknologi besar seperti Google, Facebook, dan Microsoft telah menggunakannya selama bertahun-tahun.
Namun, seringkali ketika mendengar “monorepo”, pikiran kita langsung tertuju pada framework berat seperti Nx atau Turborepo. Padahal, untuk banyak kasus, Anda bisa mendapatkan manfaat monorepo yang signifikan hanya dengan memanfaatkan fitur workspaces yang sudah ada di package manager favorit Anda (npm, Yarn, atau pnpm).
Artikel ini akan memandu Anda membangun monorepo yang efisien menggunakan workspaces. Kita akan membahas struktur dasar, cara mengelola dependensi antar paket, otomatisasi tugas, serta tantangan umum yang mungkin Anda hadapi. Tujuannya? Agar Anda bisa merasakan manfaat monorepo tanpa perlu investasi besar pada tooling yang kompleks di awal. Mari kita mulai! 🚀
2. Apa Itu Monorepo dan Mengapa Menggunakannya?
Sebelum menyelam lebih dalam ke implementasi, mari kita pahami kembali apa itu monorepo dan mengapa banyak tim mengadopsinya.
Monorepo adalah sebuah strategi pengelolaan kode di mana Anda menyimpan beberapa proyek yang saling terkait dalam satu repositori Git tunggal. Bayangkan seperti sebuah rumah besar dengan banyak kamar (proyek), tapi semuanya berada di bawah satu atap yang sama.
✅ Keuntungan Utama Monorepo:
- Code Sharing yang Mudah: Ini adalah salah satu keuntungan terbesar. Anda bisa dengan mudah membuat library utilitas atau komponen UI yang bisa digunakan kembali oleh semua proyek di dalam monorepo tanpa perlu mempublikasikannya ke npm registry eksternal.
- Atomic Commits & Perubahan Terkoordinasi: Jika Anda perlu mengubah API backend dan menyesuaikan frontend yang menggunakannya, Anda bisa melakukan kedua perubahan tersebut dalam satu commit tunggal. Ini memastikan bahwa semua bagian aplikasi selalu kompatibel satu sama lain.
- Single Source of Truth: Semua kode dan konfigurasi (misalnya, ESLint, Prettier, TypeScript) bisa disimpan di satu tempat, mengurangi duplikasi dan memastikan konsistensi.
- Penyederhanaan Manajemen Dependensi: Dengan hoisting (akan kita bahas nanti), banyak dependensi bisa diinstal hanya sekali di root monorepo, menghemat ruang disk dan waktu instalasi.
- Refactoring Lebih Aman: Ketika Anda melakukan refactoring pada shared library, Anda bisa dengan cepat melihat dan menguji dampak perubahannya pada semua proyek yang menggunakannya.
- Pengalaman Developer yang Lebih Baik (DX): Developer bisa berpindah antar proyek dengan lebih cepat karena semua kode ada di satu tempat dan proses build/test bisa diorkestrasi dari satu titik.
❌ Kapan Monorepo Mungkin Bukan Pilihan Terbaik:
- Ukuran Repositori: Monorepo bisa tumbuh sangat besar, membuat operasi Git lambat.
- Akses Kontrol: Jika Anda perlu batasan akses yang sangat ketat untuk proyek tertentu, monorepo bisa jadi tantangan.
- Build Time: Dengan banyak proyek, proses build bisa memakan waktu lama jika tidak dioptimalkan dengan baik.
Meski ada tantangan, untuk sebagian besar tim dengan beberapa aplikasi dan library internal, manfaat monorepo jauh lebih besar. Dan dengan workspaces, kita bisa memulai dengan ringan.
3. Memulai dengan Workspaces: Konfigurasi Dasar
Workspaces adalah fitur bawaan dari package manager modern (npm versi 7+, Yarn versi 1.x klasik dan 2+ Berry, pnpm). Fitur ini memungkinkan Anda untuk mengelola beberapa paket (proyek) dalam satu repositori tunggal.
📌 Struktur Folder
Mari kita bayangkan struktur monorepo dasar kita:
my-monorepo/
├── package.json # Root package.json (monorepo configuration)
├── yarn.lock # atau package-lock.json / pnpm-lock.yaml
├── packages/ # Folder untuk semua proyek/paket
│ ├── frontend/ # Aplikasi frontend (misal: React app)
│ │ └── package.json
│ ├── backend/ # Aplikasi backend (misal: Express/NestJS API)
│ │ └── package.json
│ └── shared-ui/ # Library komponen UI bersama
│ └── package.json
└── README.md
💡 Konfigurasi package.json di Root
Langkah pertama adalah mengkonfigurasi package.json di root repositori untuk mendeklarasikan folder mana yang merupakan bagian dari monorepo Anda.
Dengan npm (v7+)
// my-monorepo/package.json
{
"name": "my-monorepo-root",
"version": "1.0.0",
"private": true, // Penting! Mencegah root dipublikasikan
"workspaces": [
"packages/*" // Ini memberitahu npm untuk mencari paket di sub-folder 'packages'
],
"scripts": {
"dev": "npm run dev --workspaces", // Contoh: Jalankan script 'dev' di semua paket
"lint": "npm run lint --workspaces"
}
}
Dengan Yarn (v1.x Klasik)
// my-monorepo/package.json
{
"name": "my-monorepo-root",
"version": "1.0.0",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"dev": "yarn workspaces run dev", // Sintaks Yarn untuk menjalankan script di workspaces
"lint": "yarn workspaces run lint"
}
}
Dengan pnpm
pnpm memiliki pendekatan sedikit berbeda dengan file pnpm-workspace.yaml di root, selain package.json.
# my-monorepo/pnpm-workspace.yaml
packages:
- 'packages/*' # Ini memberitahu pnpm untuk mencari paket di sub-folder 'packages'
Dan package.json di root:
// my-monorepo/package.json
{
"name": "my-monorepo-root",
"version": "1.0.0",
"private": true,
"scripts": {
"dev": "pnpm -r dev", // Sintaks pnpm untuk menjalankan script di workspaces
"lint": "pnpm -r lint"
}
}
Penjelasan:
"private": truesangat penting. Ini mencegah package manager mencoba mempublikasikan root monorepo Anda ke registry. Hanya paket-paket di dalampackages/yang bisa dipublikasikan (jika Anda memang berniat begitu)."workspaces": ["packages/*"]adalah jantung konfigurasi. Ini memberitahu package manager untuk mencaripackage.jsondi dalam setiap sub-folder dipackages/. Anda bisa menggunakan glob pattern yang lebih spesifik, misalnya["packages/frontend", "packages/backend"]atau["apps/*", "libs/*"].
Setelah konfigurasi ini, jalankan npm install (atau yarn install / pnpm install) di root. Package manager akan menemukan semua paket di dalam packages/ dan menginstal dependensi mereka.
4. Mengelola Dependensi Antar Paket
Salah satu kekuatan monorepo adalah kemudahan berbagi kode antar proyek. Anda bisa menggunakan paket shared-ui di frontend seolah-olah itu adalah dependensi eksternal dari npm registry, padahal itu berada di folder sebelah.
🎯 Mendeklarasikan Dependensi Internal
Untuk menggunakan paket internal, cukup tambahkan ke dependencies (atau devDependencies, peerDependencies) di package.json paket yang menggunakannya, seperti biasa.
// my-monorepo/packages/frontend/package.json
{
"name": "frontend",
"version": "1.0.0",
"private": true,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"shared-ui": "workspace:^1.0.0" // Ini adalah dependensi internal!
},
"scripts": {
"dev": "react-scripts start",
"build": "react-scripts build",
"lint": "eslint ."
}
}
// my-monorepo/packages/shared-ui/package.json
{
"name": "shared-ui",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"react": "^18.2.0"
},
"scripts": {
"build": "tsc",
"lint": "eslint ."
}
}
Penjelasan:
"shared-ui": "workspace:^1.0.0": Prefiksworkspace:(didukung oleh npm, Yarn, dan pnpm) adalah cara eksplisit untuk memberitahu package manager bahwa ini adalah dependensi internal dalam monorepo. Angka^1.0.0adalah versi yang diharapkan, sama seperti dependensi eksternal. Package manager akan membuat symlink darinode_modulesfrontendke foldershared-uidipackages/.- Setelah menambahkan dependensi internal, jalankan
npm install(atauyarn install/pnpm install) di root monorepo Anda.
⚠️ Hoisting Dependensi
Ketika Anda menjalankan npm install di root monorepo, package manager akan mencoba melakukan hoisting. Ini berarti dependensi yang sama yang dibutuhkan oleh beberapa paket akan diinstal hanya sekali di node_modules root monorepo. Ini menghemat ruang disk dan mempercepat instalasi.
Namun, hoisting juga bisa membawa tantangan:
- Version Conflicts: Jika
frontendmembutuhkanlodash@4.17.20danbackendmembutuhkanlodash@4.17.21, package manager akan mencoba mencari versi yang kompatibel atau menginstal keduanya di level paket masing-masing (bukan di root). Ini bisa jadi kompleks. - “Phantom” Dependencies: Terkadang, sebuah paket mungkin secara tidak sengaja bergantung pada dependensi yang di-hoist dari paket lain, padahal tidak dideklarasikan di
package.json-nya sendiri. Ini bisa menyebabkan masalah jika strukturnode_modulesberubah.
Tips:
- Selalu deklarasikan semua dependensi eksplisit di
package.jsonsetiap paket. Jangan berasumsi dependensi akan di-hoist. - Gunakan
pnpmjika memungkinkan. pnpm memiliki strategi hoisting yang lebih ketat, yang membantu mencegah masalah “phantom” dependencies dan memastikan setiap paket hanya memiliki akses ke dependensi yang dideklarasikan secara eksplisit.
5. Otomatisasi Tugas dengan Script
Mengelola banyak proyek berarti Anda perlu cara yang efisien untuk menjalankan script (misalnya build, test, lint, dev) di satu atau semua paket. Package manager dengan fitur workspaces menyediakan cara mudah untuk melakukannya.
📝 Menjalankan Script di Semua Paket
Seperti yang sudah kita lihat di bagian konfigurasi root, Anda bisa menjalankan script di semua paket secara bersamaan.
Dengan npm (v7+)
npm run <script-name> --workspaces
# Contoh: Jalankan script 'build' di semua paket
npm run build --workspaces
Dengan Yarn (v1.x Klasik)
yarn workspaces run <script-name>
# Contoh: Jalankan script 'build' di semua paket
yarn workspaces run build
Dengan pnpm
pnpm -r <script-name>
# Contoh: Jalankan script 'build' di semua paket
pnpm -r build
🎯 Menjalankan Script di Paket Spesifik
Anda juga bisa menjalankan script hanya pada paket tertentu.
Dengan npm (v7+)
npm run <script-name> --workspace=<package-name>
# Contoh: Jalankan script 'dev' hanya di paket 'frontend'
npm run dev --workspace=frontend
Dengan Yarn (v1.x Klasik)
yarn workspace <package-name> run <script-name>
# Contoh: Jalankan script 'dev' hanya di paket 'frontend'
yarn workspace frontend run dev
Dengan pnpm
pnpm --filter <package-name> <script-name>
# Contoh: Jalankan script 'dev' hanya di paket 'frontend'
pnpm --filter frontend dev
💡 Script di Root package.json untuk Orkestrasi
Untuk workflow yang lebih kompleks, Anda bisa membuat script di package.json root yang mengorkestrasi script dari berbagai paket.
// my-monorepo/package.json
{
// ...
"scripts": {
"start-all": "npm run dev --workspace=backend & npm run dev --workspace=frontend",
"build-all": "npm run build --workspace=shared-ui && npm run build --workspaces --if-present",
"test-all": "npm test --workspaces --if-present"
}
}
Penjelasan:
start-all: Menjalankan backend dan frontend secara paralel (menggunakan&).build-all: Memastikanshared-uidibangun terlebih dahulu (karenafrontendmungkin bergantung padanya), kemudian membangun semua paket lain.&&memastikanshared-uiselesai sebelum yang lain dimulai.--if-presentmemastikan script hanya dijalankan jika ada dipackage.jsonpaket tersebut.test-all: Menjalankan semua tes di semua paket.
Ini adalah dasar-dasar otomatisasi yang sangat kuat dan cukup untuk banyak skenario monorepo awal.
6. Tantangan Umum dan Solusinya
Meskipun workspaces sangat membantu, ada beberapa tantangan umum yang mungkin Anda temui saat mengelola monorepo:
1. Order Build dan Dependensi Antar Paket
Jika frontend bergantung pada shared-ui, Anda harus memastikan shared-ui dibangun sebelum frontend.
Solusi:
- Manual Ordering: Seperti contoh
build-alldi atas, Anda bisa secara manual menentukan urutan di script root. - Package Manager Otomatis: Beberapa package manager (terutama pnpm) dan tools monorepo (seperti Lerna, Nx) secara otomatis dapat mendeteksi dependensi antar paket dan membangunnya dalam urutan yang benar. Misalnya, dengan pnpm,
pnpm -r buildakan mencoba membangun paket dengan dependensi internal terlebih dahulu.
2. Konfigurasi Bersama (ESLint, Prettier, TypeScript)
Menjaga konfigurasi yang konsisten di semua paket bisa jadi rumit.
Solusi:
- Shared Config Packages: Buat paket khusus di
packages/(misalnyaeslint-config-custom,tsconfig-custom) yang berisi konfigurasi bersama. Paket-paket lain kemudian bisa menginstal dan memperluas konfigurasi ini. - Root Configuration: Untuk beberapa tools, Anda bisa menempatkan file konfigurasi di root monorepo (misalnya
.eslintrc.js,tsconfig.json) dan mengkonfigurasinya untuk mencari file di sub-folder.
3. Testing Across Packages
Menjalankan tes untuk semua proyek atau hanya proyek yang terdampak perubahan.
Solusi:
--workspaces/-r: Gunakan perintah package manager untuk menjalankan semua tes.- Perubahan yang Terdampak: Untuk skala yang lebih besar, Anda bisa menggunakan tools seperti
lerna exec --since <git-ref> npm testatau fitur caching dari Nx/Turborepo untuk hanya menjalankan tes pada paket yang benar-benar berubah atau terdampak.
4. Lingkungan Pengembangan Lokal
Menjalankan beberapa aplikasi (misalnya frontend dan backend) secara bersamaan.
Solusi:
- Script Orkestrasi: Buat script di root
package.jsonsepertistart-allyang menjalankandevscript dari masing-masing paket secara paralel (misalnya denganconcurrentlyatau menggunakan&di Linux/macOS). - Dev Containers: Gunakan Dev Containers (seperti yang didukung VS Code) untuk menyediakan lingkungan pengembangan yang konsisten dan terisolasi untuk seluruh monorepo.
7. Kapan Menggunakan Workspaces Saja vs. Framework Monorepo (Nx/Turborepo)
Workspaces adalah fondasi yang sangat baik untuk monorepo kecil hingga menengah. Mereka menyediakan fungsionalitas inti untuk manajemen paket dan eksekusi script.
Namun, seiring pertumbuhan monorepo Anda, Anda mungkin akan mulai merasakan keterbatasan workspaces murni: