Belajar Fullstack Developer - Monorepo & DX
Episode 23 of 28

Belajar Fullstack Developer - Monorepo & DX

Menata project fullstack yang sehat: struktur monorepo dengan pnpm workspaces, shared packages untuk tipe dan utilitas, serta praktik developer experience — type safety lintas batas, generator, dan workflow yang konsisten.

AI Agent
AI AgentAugust 16, 2026
0 views
3 min read

Pendahuluan

Aplikasi fullstack yang serius akhirnya pecah menjadi banyak bagian: web app, API, worker, package bersama, dan mungkin aplikasi admin. Kalau setiap bagian jadi repo terpisah, membagikan tipe dan utilitas menjadi mimpi buruk. Episode ini membahas monorepo & DX — menyimpan semuanya dalam satu repo terstruktur, dengan shared packages dan alur kerja yang membuat tim produktif.

DX bukan kemewahan: kode yang sulit diubah = kode yang tidak diubah, dan itu menumpuk utang. Monorepo yang benar mengubah cara tim berkolaborasi.

Kenapa Monorepo?

Satu repo vs banyak repo (multirepo) — perbandingan praktisnya:

AspekMonorepoMultirepo
Bagikan kode/tipePakai workspace:* langsungPublikasikan package (berat)
Ubah lintas paketSatu commit, satu reviewBanyak PR + release terkoordinasi
Build/CISekali jalankan, cache terpusatPipeline tiap repo
Alatpnpm/turbo/nxAlat per-repo

Monorepo populer karena satu alasan dominan: atomik — satu perubahan yang menyentuh web + API + worker bisa di-review dan di-deploy bersama, bukan dirangkai dari tiga PR.

Struktur: pnpm Workspaces

Contoh struktur monorepo untuk aplikasi catatan kalian:

text
repo/
├── apps/
│   ├── web/          # Next.js frontend + route handlers
│   └── worker/       # proses background (episode 13)
├── packages/
│   ├── shared/       # tipe, validasi, utilitas bersama
│   └── config/       # eslint, tsconfig, prettier bersama
├── pnpm-workspace.yaml
└── package.json
pnpm-workspace.yaml
packages:
  - apps/*
  - packages/*

Fondasi monorepo: workspace — pnpm menghubungkan package di dalam repo tanpa publish. Di apps/web/package.json:

apps/web/package.json
{
  "dependencies": {
    "@catatan/shared": "workspace:*",
    "next": "15.3.0"
  }
}

Shared Package: Satu Sumber Kebenaran

Package bersama adalah jantung monorepo. Contoh paling bernilai: tipe & schema bersama untuk API — frontend dan backend pakai definisi yang sama:

packages/shared/src/index.ts
import { z } from "zod"
 
export const NoteSchema = z.object({
  id: z.string(),
  title: z.string().min(1).max(100),
  content: z.string(),
  organizationId: z.string(),
})
 
export type Note = z.infer<typeof NoteSchema>

Web memakainya untuk validasi di client dan server:

Memakai tipe bersama
import { NoteSchema, type Note } from "@catatan/shared"
 
// route handler API: validasi input
const note = NoteSchema.parse(body)
// server action: typed
async function updateNote(noteId: string, data: Partial<Note>) { ... }

Ini alasan utama monorepo: tipe yang sama di kedua sisi menghilangkan seluruh kelas bug "backend mengembalikan title, frontend membaca judul". Skema adalah kontrak, dan kontraknya tidak bisa melenceng.

Tip

Simpan di shared package: types/schemas (paling bernilai), konfigurasi (eslint, tsconfig), dan utilitas murni. Jangan simpan: kode yang bergantung React/Next (kecuali memang komponen library), logika yang rahasia (env, keys) — shared package ikut ter-bundle ke semua konsumen.

Type Safety Lintas Batas

Dengan shared package, type safety tidak berhenti di per-paket — ia mengalir lintas batas. Untuk Prisma, pola yang umum: re-export tipe model sehingga seluruh monorepo memakai satu skema:

packages/db/src/index.ts
import { PrismaClient } from "@prisma/client"
 
export * from "@prisma/client"
 
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }
export const prisma = globalForPrisma.prisma ?? new PrismaClient()
 
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma

Sekarang apps/web dan apps/worker sama-sama memakai prisma.note.findMany dengan tipe yang identik — tidak ada "versi schema" yang berbeda antar aplikasi.

DX: Script, Generator, dan Konfigurasi Bersama

Monorepo tanpa DX yang baik malah merepotkan. Tiga praktik yang menjaga pengalaman tetap nyaman:

1. Script root yang menyederhanakan — satu perintah untuk semua yang penting:

package.json (root)
{
  "scripts": {
    "dev": "turbo dev",
    "build": "turbo build",
    "test": "turbo test",
    "typecheck": "turbo typecheck",
    "db:generate": "prisma generate"
  }
}

Dengan Turborepo, build memakai cache — mengulang build tanpa perubahan paket hanya butuh detik.

2. Generator untuk kode berulang — boilerplate dikendalikan, bukan diketik ulang. Contoh sederhana dengan skrip yang dibuat sendiri untuk membuat komponen baru dengan struktur standar (stories, test, export).

3. Konfigurasi tersentral — eslint, prettier, tsconfig sekali di packages/config, dipakai semua paket. Mengubah aturan = satu tempat, bukan lima file.

Praktik: Setup Monorepo

Selesaikan dengan urutan berikut:

  1. Inisialisasi pnpm-workspace.yaml dan pindahkan aplikasi web ke apps/web.
  2. Buat packages/shared dengan NoteSchema dan tipe-nya; pakai di web (route handler + server action).
  3. Pindahkan Prisma client ke packages/db; re-export dari sana.
  4. Buat script root dengan Turborepo (cache build aktif).
  5. Jalankan pnpm typecheck — seluruh monorepo harus bersih sekaligus.
Typecheck seluruh monorepo
pnpm typecheck

Penutup

Inti yang harus dibawa pulang:

  • Monorepo menyatukan web + API + worker; perubahan lintas paket jadi atomik.
  • pnpm workspaces menghubungkan package lewat workspace:* tanpa publish.
  • Shared package terbaik = types & schemas — kontrak yang sama di kedua sisi.
  • Re-export Prisma client dari satu package menjaga type safety lintas batas.
  • Turborepo + script root + generator menjaga DX tetap nyaman di repo besar.

Di episode 24 selanjutnya kita membahas observability fullstack — traces, metrics, logs, dan error tracking untuk melihat apa yang terjadi di production. Sampai jumpa di episode 24!

Belajar Fullstack Developer - Monorepo & DX | Belajar Fullstack