Arsitektur Modular 8-File
Pemisahan tegas antara folder routing murni di app/ dan logika bisnis fitur yang terisolasi dalam 8 file standar di modules/.
Membaca sekitar 2 menit
Pemisahan tegas antara folder routing murni di app/ dan logika bisnis fitur yang terisolasi dalam 8 file standar di modules/.
Setiap halaman di app/ hanya menjadi pembungkus tipis ber-Suspense, sementara validasi Zod, query Prisma, service transaksi, Server Action, dan React Query hooks tinggal di modul masing-masing.
Ketika struktur tiap fitur seragam 100%, baik developer baru maupun asisten AI tidak lagi menebak-nebak di mana harus menaruh kode.
Masalahnya dulu
Ketika proyek Next.js App Router mulai tumbuh melewati belasan halaman, batas antara kode tampilan, query database, dan aturan bisnis sering kali kabur. File page.tsx di dalam folder app/ berubah menjadi tempat penampungan segalanya: mulai dari query Prisma langsung, validasi input, hingga ratusan baris komponen JSX. Akibatnya, saat ingin mengubah satu aturan bisnis atau meminta bantuan asisten AI memodifikasi fitur, kita harus melompat-lompat di antara banyak folder yang tidak memiliki pola seragam.
Memisahkan panggung routing dari dapur modul
Di proyek ini, direktori app/ diperlakukan murni sebagai lapisan routing tipis. Ia hanya boleh berisi layout, metadata, Suspense boundary dengan skeleton loading, dan pemanggilan satu komponen halaman dari folder modules/. Seluruh logika sesungguhnya dipecah berdasarkan audiens (modules/public/ dan modules/dashboard/), di mana setiap fitur dashboard wajib memiliki 8 file dengan tanggung jawab tunggal yang ketat: schema, service, queries, actions, hooks, Modal, Table, dan Page.
Folder app/ hanyalah peta jalan bagi URL, sedangkan seluruh mesin bisnis yang sesungguhnya bekerja dengan rapi di dalam modules/.
Implementasi di ngodingan ini
Perhatikan betapa tipis dan bersihnya sebuah route di app/(dashboard)/dashboard/items/page.tsx karena seluruh beban autentikasi halaman dan struktur UI sudah didelegasikan ke modules/dashboard/item/pages/ItemPage.tsx:
// app/(dashboard)/dashboard/items/page.tsx
import { Suspense } from "react";
import DashboardPageSkeleton from "@/common/components/skeletons/DashboardPageSkeleton";
import ItemPage from "@/modules/dashboard/item/pages/ItemPage";
export const instant = false;
export default function Page() {
return (
<Suspense fallback={<DashboardPageSkeleton />}>
<ItemPage />
</Suspense>
);
}
// modules/dashboard/item/pages/ItemPage.tsx
import { Package } from "lucide-react";
import { requirePageAccess } from "@/common/lib/auth";
import PageHeader from "@/common/components/PageHeader";
import ItemTable from "../components/ItemTable";
export default async function ItemPage() {
await requirePageAccess("item");
return (
<div className="space-y-6">
<PageHeader
title="Master Data Item"
description="Kelola daftar item referensi sebagai contoh standar modul CRUD enterprise."
icon={Package}
/>
<ItemTable />
</div>
);
}Jebakan dan detail yang harus dijaga
- Mencegah kebocoran kode server ke bundle browser: Setiap file *.service.ts dan *.queries.ts wajib diawali baris import "server-only" agar build langsung menolak jika tidak sengaja diimpor oleh komponen klien.
- Menghindari dynamic prerendering error di Next.js 16: Semua halaman yang bergantung pada cookie sesi atau database mengekspor export const instant = false dan dibungkus <Suspense> agar proses build produksi berjalan mulus.