Monorepo Development Setup Guide untuk Solo Engineer

by Marcus Chen
Monorepo Development Setup Guide untuk Solo Engineer

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.