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.

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.
Satu repo vs banyak repo (multirepo) — perbandingan praktisnya:
| Aspek | Monorepo | Multirepo |
|---|---|---|
| Bagikan kode/tipe | Pakai workspace:* langsung | Publikasikan package (berat) |
| Ubah lintas paket | Satu commit, satu review | Banyak PR + release terkoordinasi |
| Build/CI | Sekali jalankan, cache terpusat | Pipeline tiap repo |
| Alat | pnpm/turbo/nx | Alat 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.
Contoh struktur monorepo untuk aplikasi catatan kalian:
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.jsonpackages:
- apps/*
- packages/*Fondasi monorepo: workspace — pnpm menghubungkan package di dalam repo tanpa publish. Di apps/web/package.json:
{
"dependencies": {
"@catatan/shared": "workspace:*",
"next": "15.3.0"
}
}Package bersama adalah jantung monorepo. Contoh paling bernilai: tipe & schema bersama untuk API — frontend dan backend pakai definisi yang sama:
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:
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.
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:
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 = prismaSekarang apps/web dan apps/worker sama-sama memakai prisma.note.findMany dengan tipe yang identik — tidak ada "versi schema" yang berbeda antar aplikasi.
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:
{
"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.
Selesaikan dengan urutan berikut:
pnpm-workspace.yaml dan pindahkan aplikasi web ke apps/web.packages/shared dengan NoteSchema dan tipe-nya; pakai di web (route handler + server action).packages/db; re-export dari sana.pnpm typecheck — seluruh monorepo harus bersih sekaligus.pnpm typecheckInti yang harus dibawa pulang:
workspace:* tanpa publish.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!