App Router di Next.js membawa perubahan arsitektur yang jauh lebih besar daripada sekadar penggantian struktur folder. Pada proyek skala besar, migrasi dari Pages Router atau struktur lama tidak bisa dilakukan dengan pendekatan “rename folder lalu selesai”. Anda perlu memikirkan ulang layout tree, batas antara Server Component dan Client Component, pola data fetching, strategi rollout, serta kompatibilitas library yang sebelumnya diasumsikan selalu berjalan di browser.
Artikel ini membahas pendekatan migrasi yang realistis untuk sistem besar: dilakukan bertahap, minim risiko, dan tetap memungkinkan deploy tanpa downtime. Fokusnya bukan hanya “cara kerja fitur”, tetapi juga mengapa struktur tertentu lebih aman dan lebih mudah dipelihara dalam jangka panjang.
Mengapa migrasi ke App Router perlu dirancang ulang, bukan sekadar dipindah
Pada Pages Router, banyak aplikasi tumbuh dengan pola yang berpusat pada halaman: pages/, getServerSideProps, getStaticProps, custom _app, dan _document. Seiring skala membesar, sering muncul masalah seperti:
- Layout global yang terlalu besar dan penuh logika.
- Data fetching tersebar di banyak halaman dengan pola yang tidak konsisten.
- Terlalu banyak komponen yang berjalan di client padahal tidak perlu.
- Navigasi modal, dashboard multi-panel, atau halaman dengan beberapa area independen menjadi sulit dimodelkan.
- Shared state dan dependency browser-only bocor ke seluruh aplikasi.
App Router memperkenalkan model berbasis segment dan layout bertingkat. Ini memungkinkan rendering yang lebih granular, pemisahan concern yang lebih jelas, serta pemanfaatan React Server Components untuk mengurangi JavaScript yang dikirim ke browser. Namun, perubahan ini juga berarti Anda perlu menata ulang asumsi dasar aplikasi.
Prinsip penting: jangan migrasikan satu per satu file secara mekanis. Migrasikan berdasarkan domain area, pola render, dan dependensi runtime.
Strategi migrasi bertahap tanpa downtime
1. Jalankan Pages Router dan App Router secara berdampingan
Pada migrasi besar, pendekatan paling aman adalah mempertahankan pages/ untuk route lama sambil menambahkan app/ untuk route baru atau route yang sudah direfactor. Next.js mendukung koeksistensi ini sehingga Anda tidak perlu melakukan big bang migration.
Strategi umumnya:
- Pertahankan route kritikal dan stabil di
pages/. - Pilih satu area yang terisolasi, misalnya dashboard internal, lalu migrasikan ke
app/. - Buat metrik observability sebelum dan sesudah migrasi: error rate, TTFB, hydration warning, bundle size, dan web vitals.
- Lakukan rollout bertahap berdasarkan route atau traffic segment.
2. Migrasikan per area bisnis, bukan per jenis file
Contoh pembagian yang lebih sehat:
- /marketing: sering cocok menjadi area statis/server-heavy.
- /dashboard: kandidat untuk layout bertingkat, parallel routes, dan data server + interaksi client.
- /settings: cocok untuk nested layout dan pemisahan form client dari shell server.
- /checkout: migrasikan paling akhir jika traffic kritikal dan sensitif.
Dengan pendekatan ini, Anda bisa fokus pada kontrak perilaku per domain, termasuk auth, caching, logging, dan SEO.
3. Pertahankan kontrak URL dan response
Untuk menghindari downtime dan dampak SEO:
- Jangan ubah path URL publik jika tidak perlu.
- Pertahankan metadata, canonical URL, status code, dan redirect lama.
- Validasi bahwa middleware, auth guard, dan analytics masih bekerja pada route baru.
App Router memberi kontrol yang lebih baik pada layout dan metadata, tetapi jika Anda mengubah terlalu banyak aspek sekaligus, proses debugging akan lebih sulit.
Strategi struktur folder untuk proyek besar
Salah satu kesalahan umum saat migrasi adalah meniru struktur pages/ ke dalam app/ tanpa memanfaatkan konsep segment. Untuk proyek besar, gunakan struktur yang mengekspresikan area bisnis, bukan hanya tipe file.
app/
(public)/
layout.tsx
page.tsx
pricing/page.tsx
blog/[slug]/page.tsx
(auth)/
login/page.tsx
register/page.tsx
(dashboard)/
dashboard/
layout.tsx
page.tsx
analytics/page.tsx
users/
page.tsx
[id]/page.tsx
@sidebar/
default.tsx
users/page.tsx
@activity/
default.tsx
page.tsx
(modal)/
.
api/
reports/route.ts
layout.tsx
error.tsx
not-found.tsxRoute groups untuk pemisahan concern
Route group seperti (public), (auth), dan (dashboard) berguna untuk mengelompokkan route tanpa memengaruhi URL. Ini sangat berguna untuk:
- Memisahkan layout berbeda pada area publik, area login, dan area aplikasi internal.
- Mencegah root layout menjadi terlalu gemuk.
- Menetapkan provider atau wrapper hanya pada subtree tertentu.
Contoh: area dashboard membutuhkan provider state, navigasi samping, dan boundary error khusus. Area publik mungkin tidak membutuhkan semua itu.
Layout bertingkat untuk menurunkan kompleksitas
Pada Pages Router, banyak tim menumpuk logic di _app.tsx. Di App Router, pindahkan logic tersebut ke layout yang relevan. Root layout sebaiknya menangani hal global saja: html, body, tema dasar, dan provider yang benar-benar lintas aplikasi.
Contoh layout dashboard:
export default function DashboardLayout({
children,
sidebar,
activity,
}: {
children: React.ReactNode
sidebar: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="dashboard-shell">
<aside>{sidebar}</aside>
<main>{children}</main>
<section>{activity}</section>
</div>
)
}Pola ini memudahkan komposisi halaman kompleks tanpa menjadikan satu halaman sebagai “super component”.
Parallel routes dan intercepting routes: kapan digunakan
Parallel routes untuk panel independen
Parallel routes cocok saat satu halaman memiliki beberapa area yang dapat dirender independen, misalnya dashboard dengan panel utama, sidebar kontekstual, dan aktivitas terbaru. Keuntungannya:
- Layout lebih deklaratif.
- Slot terpisah bisa memiliki loading/error boundary masing-masing.
- Refactor per panel menjadi lebih aman pada tim besar.
Namun, jangan gunakan parallel routes hanya karena “fiturnya ada”. Jika UI Anda sederhana, nested layout biasa biasanya lebih mudah dipelihara.
Intercepting routes untuk modal berbasis navigasi
Intercepting routes berguna untuk pola seperti klik item daftar lalu detail muncul sebagai modal, tetapi saat URL dibuka langsung tetap menjadi halaman penuh. Ini sangat membantu untuk pengalaman pengguna sekaligus menjaga deep-linking.
Contoh kasus: dari /dashboard/users, klik user membuka modal detail user tanpa meninggalkan konteks daftar. Tetapi jika seseorang membuka URL user langsung, ia mendapatkan halaman detail penuh.
Trade-off-nya adalah kompleksitas navigasi meningkat. Tim harus paham perbedaan state navigasi “dari dalam aplikasi” versus akses langsung dari browser.
Refactor halaman client-heavy menjadi Server + Client Components
Salah satu manfaat terbesar App Router adalah kemampuan memindahkan bagian non-interaktif ke server. Banyak halaman lama menggunakan useEffect untuk fetch data, menyimpan loading state, lalu merender tabel atau konten yang sebenarnya tidak memerlukan interaksi berat. Ini membuat bundle client membesar dan memperlambat render awal.
Sebelum: halaman berat di client
'use client'
import { useEffect, useState } from 'react'
export default function UsersPage() {
const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then((res) => res.json())
.then((data) => setUsers(data))
.finally(() => setLoading(false))
}, [])
if (loading) return <p>Loading...</p>
return (
<div>
<h1>Users</h1>
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}Masalah dari pendekatan ini:
- Data baru diambil setelah JavaScript client berjalan.
- UI awal sering hanya loading state.
- Semua rendering dipaksa ke client meskipun daftar user bisa dirender di server.
Sesudah: shell server, interaksi di client
import UsersTable from './UsersTable'
import UserFilters from './UserFilters'
async function getUsers(searchParams: { q?: string }) {
const query = new URLSearchParams()
if (searchParams.q) query.set('q', searchParams.q)
const res = await fetch(`${process.env.API_BASE_URL}/users?${query.toString()}`, {
headers: {
Authorization: `Bearer ${process.env.INTERNAL_API_TOKEN}`,
},
cache: 'no-store',
})
if (!res.ok) {
throw new Error('Failed to fetch users')
}
return res.json()
}
export default async function UsersPage({
searchParams,
}: {
searchParams: Promise<{ q?: string }>
}) {
const params = await searchParams
const users = await getUsers(params)
return (
<div>
<h1>Users</h1>
<UserFilters initialQuery={params.q ?? ''} />
<UsersTable users={users} />
</div>
)
}'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { useTransition } from 'react'
export default function UserFilters({ initialQuery }: { initialQuery: string }) {
const router = useRouter()
const searchParams = useSearchParams()
const [isPending, startTransition] = useTransition()
return (
<input
defaultValue={initialQuery}
placeholder="Cari user"
onChange={(e) => {
const params = new URLSearchParams(searchParams.toString())
const value = e.target.value
if (value) params.set('q', value)
else params.delete('q')
startTransition(() => {
router.replace(`/dashboard/users?${params.toString()}`)
})
}}
aria-busy={isPending}
/>
)
}Pada refactor ini, data utama diambil oleh Server Component, sedangkan input filter tetap di Client Component. Hasilnya:
- Render awal lebih cepat dan lebih stabil.
- Bundle client lebih kecil.
- Secret atau token internal tidak bocor ke browser.
- Pemisahan tanggung jawab lebih jelas.
Perubahan pola data fetching yang perlu dipahami
Dari getServerSideProps ke async Server Components
Di App Router, banyak kasus yang sebelumnya dihandle oleh getServerSideProps kini dipindahkan ke komponen server async, route handler, atau helper server-side. Perubahan mental model ini penting:
- Fetch data sedekat mungkin dengan komponen yang membutuhkannya.
- Gunakan boundary loading dan error di level segment bila perlu.
- Pahami kapan data boleh di-cache dan kapan harus selalu fresh.
Pada proyek besar, sebaiknya buat lapisan akses data yang eksplisit, misalnya lib/server/ atau modules/*/server/, agar logika auth header, observability, retry, dan error mapping tidak tersebar.
Bedakan data publik, personal, dan mutable
Kesalahan umum adalah menerapkan strategi cache yang sama untuk semua data. Secara praktis:
- Data publik dan jarang berubah: cocok untuk caching lebih agresif.
- Data personal per user: hati-hati terhadap cache bersama.
- Data yang sering berubah: gunakan pendekatan yang memastikan konsistensi, misalnya
no-storebila diperlukan.
Jangan asal memindahkan semua fetch lama ke server tanpa meninjau sifat data dan kebutuhan invalidasi.
Risiko umum saat migrasi dan cara mengatasinya
Hydration mismatch
Masalah ini sering muncul ketika output server berbeda dengan output client. Penyebab umum:
- Menggunakan
Date.now(),Math.random(), atau waktu lokal langsung saat render. - Mengakses
window,localStorage, atau ukuran layar pada komponen yang tidak ditandai sebagai client. - Conditional render berdasarkan state browser yang belum tersedia di server.
Solusi praktis:
- Pindahkan logika browser-only ke Client Component.
- Gunakan fallback yang konsisten antara server dan client.
- Audit komponen yang menghasilkan markup nondeterministik.
Dependency browser-only
Banyak library lama mengasumsikan adanya window atau DOM saat import. Saat komponen dijadikan Server Component, import semacam ini bisa gagal.
Tanda-tandanya:
- Error saat build atau runtime di server.
- Pesan seperti window is not defined.
- Komponen pihak ketiga mendadak rusak setelah dipindah ke App Router.
Mitigasi:
- Batasi library browser-only di file dengan
'use client'. - Jika perlu, bungkus library tersebut dalam adapter client.
- Jangan menambahkan
'use client'ke terlalu banyak file hanya untuk “memperbaiki cepat”, karena ini bisa mengorbankan manfaat App Router.
Provider terlalu tinggi di tree
Sering terjadi tim memindahkan semua provider lama ke root layout. Akibatnya, subtree besar menjadi client-heavy atau kehilangan peluang optimasi server.
Praktik yang lebih baik:
- Letakkan provider sedekat mungkin ke area yang membutuhkannya.
- Pisahkan provider global yang benar-benar lintas aplikasi dari provider fitur.
- Evaluasi apakah provider tertentu masih perlu setelah data dipindah ke server.
Testing, observability, dan debugging saat rollout
Migrasi besar yang aman membutuhkan validasi teknis, bukan hanya pengujian visual.
Checklist sebelum memindahkan route
- Bandingkan HTML dan metadata route lama vs route baru.
- Pastikan auth, redirect, dan access control identik.
- Uji direct access, hard refresh, client navigation, dan deep link.
- Uji skenario loading, error boundary, dan not-found.
- Audit bundle client agar tidak membengkak setelah refactor.
Debugging yang paling sering membantu
- Lihat warning hydration di console browser dan cocokkan dengan markup awal.
- Periksa apakah suatu file tanpa sengaja menjadi client karena import dari module client.
- Audit network request ganda akibat fetch yang dilakukan di server dan client sekaligus.
- Pastikan environment variable rahasia hanya diakses dari jalur server.
Untuk proyek besar, tambahkan logging terstruktur pada route handler dan helper data access. Saat migrasi berlangsung, observability lebih penting daripada optimasi mikro.
Rekomendasi rencana migrasi yang realistis
- Inventarisasi route: kelompokkan berdasarkan criticality, pola render, dan dependensi browser-only.
- Tentukan domain pilot: pilih area dengan risiko sedang tetapi cukup representatif, misalnya dashboard admin.
- Desain ulang folder app/: gunakan route groups, nested layout, dan segment yang sesuai domain.
- Pindahkan data fetching ke server secara bertahap: mulai dari halaman yang paling banyak memuat data non-interaktif.
- Isolasi Client Components: simpan interaksi, event handler, dan browser API hanya di bagian yang benar-benar memerlukannya.
- Uji koeksistensi pages/ dan app/: validasi middleware, analytics, auth, dan SEO.
- Rollout bertahap: monitor error, performa, dan perilaku navigasi sebelum memperluas cakupan migrasi.
Migrasi ke App Router di Next.js 16 pada proyek besar adalah pekerjaan arsitektural, bukan sekadar perubahan sintaks. Jika dilakukan dengan strategi yang tepat, hasilnya bukan hanya struktur folder baru, tetapi aplikasi yang lebih modular, lebih efisien di client, dan lebih mudah dikembangkan lintas tim. Kunci utamanya adalah migrasi bertahap, pemisahan tanggung jawab yang jelas antara server dan client, serta disiplin dalam mengelola layout, data fetching, dan dependensi runtime.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!