Struktur Proyek Next.js yang Skalabel dengan App Router

Foto oleh Unsplash

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?