Struktur Proyek Next.js yang Skalabel dengan App Router

Foto oleh Unsplash
Route group — folder yang dibungkus dalam tanda kurung — memungkinkan kamu mengorganisir route dan berbagi layout tanpa menambahkan segmen apapun ke URL. Misalnya, menempatkan halaman di bawah app/[locale]/(pages)/ berarti /en/about diselesaikan dari app/[locale]/(pages)/about/page.tsx, menjaga URL tetap bersih. Kamu juga bisa menggunakan route group terpisah seperti (dashboard) dan (marketing) untuk menerapkan layout berbeda pada bagian app yang berbeda.
Aturannya sederhana: jika komponen hanya digunakan oleh satu route, kolokasikan bersama route itu. Begitu dua atau lebih route berbagi komponen, pindahkan ke app/components/. Misalnya, BlogCard dan BlogFilters berada di dalam folder route blog, sementara Navbar dan Footer — yang digunakan di setiap halaman — ada di app/components/layout/.
Import relatif seperti '../../../lib/db' menjadi mimpi buruk pemeliharaan seiring proyek berkembang, terutama di tim dengan lebih dari tiga developer. Mengonfigurasi paths tsconfig.json sekali (misalnya @/lib/db) dan menggunakan barrel export (index.ts) berarti reorganisasi file internal tidak pernah memutus import statement di tempat lain. Barrel export juga memungkinkan import yang bersih seperti '{ Button, Input, Modal }' from '@/components/ui' daripada path per-file yang dalam.
Dalam mode development Next.js, hot module reload dapat memicu banyak instansiasi PrismaClient. Setiap instance membuka connection pool database-nya sendiri, sehingga tanpa pola singleton, pool habis dalam hitungan menit. Singleton memastikan hanya ada satu instance PrismaClient per proses Node.js, menjaga penggunaan koneksi tetap terprediksi.
Memusatkan semua parsing env var di app/lib/env.ts — yang divalidasi dengan t3-env atau pola berbasis zod — menangkap variabel yang hilang seperti DATABASE_URL atau STRIPE_SECRET_KEY saat aplikasi startup, bukan saat pengguna memicu code path di runtime. Mengekspor konstanta bertipe dari file tersebut juga menghilangkan akses process.env mentah yang tersebar di seluruh codebase, memberikan autocomplete yang tepat dan mencegah miskonfigurasi diam yang muncul di produksi.

Foto oleh Unsplash
Setiap proyek Next.js dimulai sederhana — beberapa halaman, beberapa komponen, mungkin folder lib. Tiga bulan kemudian, kamu mencari fungsi utilitas yang terkubur di suatu komponen, dan direktori app-mu terlihat seperti gudang digital yang berantakan. Saya sudah membangun ulang struktur ini dua kali di proyek nyata, dan pola yang saya bagikan ini adalah yang benar-benar saya gunakan di Commsult untuk aplikasi web komersial.
App Router memperkenalkan model mental yang berbeda secara fundamental. File di dalam app/ bisa berupa Server Components, Client Components, layout, loading state, error boundaries, atau API routes — semuanya berkolokasi. Tanpa strategi folder yang disengaja, ini menjadi kacau dengan cepat. Struktur yang saya gunakan memisahkan concern dengan jelas: routing ada di app/[locale]/(pages)/, komponen bersama di app/components/, utilitas di app/lib/, dan seterusnya.
Route group (folder yang dibungkus dalam tanda kurung) memungkinkan kamu mengorganisir route tanpa memengaruhi URL. Saya menggunakan (pages) untuk mengelompokkan semua halaman yang dihadapi pengguna di bawah awalan locale tanpa menambahkan 'pages' ke URL. Ini berarti /en/about diselesaikan dari app/[locale]/(pages)/about/page.tsx. Kamu juga bisa menggunakan route group untuk menerapkan layout berbeda — misalnya, grup (dashboard) dengan layout sidebar terpisah dari halaman (marketing) dengan navbar biasa.
Perdebatan umum: apakah komponen fitur harus berada di samping route-nya, atau di folder components terpusat? Aturan saya: jika komponen hanya digunakan oleh satu route, kolokasikan. Jika dibagi dua atau lebih route, pindahkan ke app/components/. Ini mencegah masalah 'di mana ini tinggal?'. Untuk blog, BlogCard dan BlogFilters hanya milik listing blog. Tapi Navbar dan Footer bersifat global, jadi mereka ada di app/components/layout/.
my-nextjs-app/
├── app/
│ ├── [locale]/
│ │ ├── (pages)/
│ │ │ ├── about/page.tsx
│ │ │ ├── blog/
│ │ │ │ ├── page.tsx # listing
│ │ │ │ └── [slug]/page.tsx # detail
│ │ │ └── contact/page.tsx
│ │ ├── layout.tsx # locale-aware root layout
│ │ └── page.tsx # home
│ ├── api/
│ │ └── [...route]/route.ts # catch-all API handler
│ ├── components/
│ │ ├── ui/ # shadcn/ui or custom primitives
│ │ ├── layout/ # Navbar, Footer, Sidebar
│ │ └── [feature]/ # feature-specific components
│ ├── hooks/ # custom React hooks
│ ├── lib/ # shared utilities, configs
│ ├── types/ # global TypeScript types
│ └── globals.css
├── messages/ # i18n JSON files (next-intl)
│ ├── en.json
│ └── id.json
├── public/
│ └── images/
├── prisma/
│ └── schema.prisma
└── next.config.tsGunakan barrel export (file index.ts) di folder komponen kamu. Daripada mengimpor dari path dalam seperti @/components/ui/Button, ekspor semuanya dari @/components/ui/index.ts dan impor { Button, Input, Modal } from '@/components/ui'. Ini membuat refactoring nama file internal menjadi mudah.
Path alias tidak bisa dinegosiasikan pada proyek apapun dengan lebih dari 3 developer atau yang akan bertahan lebih dari sebulan. Import relatif seperti '../../../lib/db' adalah mimpi buruk pemeliharaan. Konfigurasikan paths tsconfig.json sekali, gunakan @/lib/db di mana saja. Pola singleton Prisma client juga kritis — menginstansiasi PrismaClient pada setiap hot module reload di development akan menghabiskan connection pool database kamu dalam hitungan menit.
Simpan semua parsing environment variable di satu tempat: app/lib/env.ts. Gunakan t3-env atau pola berbasis zod untuk memvalidasi env var saat startup, bukan saat runtime ketika pengguna memicu code path. Ini menangkap STRIPE_SECRET_KEY atau DATABASE_URL yang hilang sebelum kamu deploy, bukan di produksi pukul 2 pagi. Ekspor konstanta bertipe dari file ini sehingga setiap konsumen mendapatkan autocomplete yang tepat.
// tsconfig.json — path aliases
{
"compilerOptions": {
"paths": {
"@/*": ["./*"],
"@/components/*": ["./app/components/*"],
"@/lib/*": ["./app/lib/*"],
"@/types/*": ["./app/types/*"],
"@/hooks/*": ["./app/hooks/*"]
}
}
}
// app/components/ui/index.ts — barrel export
export { Button } from "./Button"
export { Input } from "./Input"
export { Modal } from "./Modal"
export { Badge } from "./Badge"
// Usage anywhere in the project
import { Button, Input, Modal } from "@/components/ui"
// app/lib/db.ts — singleton Prisma client
import { PrismaClient } from "@prisma/client"
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined
}
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["query"] : [],
})
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prismaBerbasis layer (controllers/, services/, repositories/) bekerja dengan baik untuk aplikasi backend NestJS. Untuk frontend Next.js, saya lebih suka pengelompokan berbasis fitur di dalam components/. Folder fitur 'products' berisi ProductCard.tsx, ProductFilters.tsx, ProductTable.tsx, dan useProducts.ts. Ini memudahkan menghapus atau memindahkan seluruh fitur tanpa mencari di banyak direktori.
Nesting route group lebih dari dua level menciptakan rantai pewarisan layout Next.js yang sulit dipahami. Jika kamu menemukan diri di app/[locale]/(pages)/(dashboard)/(settings)/profile/page.tsx, pertimbangkan untuk meratakan. Route group adalah untuk berbagi layout, bukan untuk mengekspresikan hierarki yang tidak ada di URL.
Segmen dinamis [locale] di root memberikan internasionalisasi tanpa kekacauan direktori i18n terpisah. Semua terjemahan ada di messages/en.json dan messages/id.json di root proyek. Library next-intl menangani sisanya — useTranslations() di Client Components, getTranslations() di Server Components. Jaga namespace kunci terjemahan konsisten dengan struktur fitur: blog.posts.*, dashboard.sidebar.*, common.* untuk label bersama.
Sebelum proyek berkembang melampaui MVP, jalankan ini: (1) Apakah semua utilitas bersama ada di lib/, bukan tersebar di components? (2) Apakah semua Server Component tetap server-side — tidak ada import useState atau useEffect? (3) Apakah Prisma diinstansiasi sebagai singleton? (4) Apakah API route menggunakan penanganan error yang tepat dengan bentuk respons yang konsisten? (5) Apakah environment variable divalidasi saat startup? (6) Apakah ada satu konfigurasi tsconfig paths yang digunakan seluruh tim?