Pertengahan 2025, Next.js 15 menjadikan App Router sebagai default yang jelas untuk proyek baru — 80% proyek Next.js baru dimulai dengannya. Tapi bagaimana dengan ratusan ribu aplikasi Pages Router yang sudah ada? Saya memigrasikan aplikasi web komersial produksi di Commsult dari Pages Router ke App Router, dan itu salah satu pengalaman paling edukatif (kadang menyakitkan) yang pernah saya alami sebagai pengembang.
Pages Router tidak akan hilang besok — Vercel masih memeliharanya. Tapi App Router membuka kemampuan yang Pages Router secara fundamental tidak bisa tawarkan: React Server Components (nol JS sisi klien untuk komponen data-fetching), Streaming dengan Suspense, parallel routes, intercepting routes, dan model caching baru. Di aplikasi komersial kami, motivasi utamanya adalah performa — Server Components memungkinkan kami memindahkan logika data-fetching yang signifikan dari klien, mengurangi Time to Interactive (TTI) secara terukur.
Hambatan terbesar bukan sintaks — melainkan berpikir dalam dua lingkungan rendering secara bersamaan. Di Pages Router, segalanya adalah komponen React dan data fetching terjadi di getServerSideProps atau getStaticProps di level halaman. Di App Router, setiap komponen secara default adalah Server Component. Begitu Anda menambahkan useState, useEffect, atau API browser apa pun, Anda perlu menandainya 'use client'. Batas ini bukan sekadar directive — ini keputusan arsitektural tentang di mana state berada.
Pages Router (before) App Router (after)
───────────────────── ──────────────────────
pages/ app/
├── index.tsx ├── [locale]/
├── about.tsx │ ├── page.tsx ← Server Component
├── blog/[slug].tsx │ ├── layout.tsx ← Shared UI
└── api/ │ └── (pages)/
└── hello.ts ├── about/
│ └── page.tsx
getServerSideProps() │ └── blog/
getStaticProps() → │ └── [slug]/page.tsx
└── api/
└── route.ts ← Route Handler
Data fetching: getServerSideProps Data fetching: async Server Component
Client state: useRouter() Client state: useSearchParams()
Dynamic route: useRouter.query Dynamic route: params prop (async)
Images: next/image Images: next/image (same)Dari pengalaman migrasi saya: mulai dengan mengaudit setiap komponen untuk useState, useEffect, dan event handler sebelum menyentuh satu file pun. Tandai dengan warna — hijau untuk render murni (bisa server), kuning untuk hook data saja, merah untuk interaksi browser. Audit ini menghemat Anda dari penyebaran 'use client' di mana Anda menandai seluruh subtree sebagai klien padahal hanya komponen daun yang membutuhkannya.
Panduan migrasi resmi Next.js bagus tapi meremehkan gesekan nyata. Dalam praktik, bagian tersulit adalah: (1) Library yang mengasumsikan global browser — library chart, rich text editor, date picker yang mengakses window atau document langsung akan error di server. (2) useRouter dari next/router sangat berbeda dari useRouter di next/navigation — nama sama, perilaku berbeda. useRouter App Router tidak bisa membaca query param; Anda perlu useSearchParams() secara terpisah. (3) API route pindah dari pages/api/ ke app/api/[...route]/route.ts dengan signature ekspor yang sama sekali berbeda.
getServerSideProps dan getStaticProps tidak ada di App Router. Server Components langsung fetch data — Anda cukup await fetch() atau panggil Prisma di bagian atas komponen. Ini lebih sederhana secara teori tapi membutuhkan pemahaman tentang caching otomatis Next.js. Secara default, fetch() di Server Components di-cache. Jika Anda butuh data segar setiap request, Anda harus menambahkan { cache: 'no-store' }. Lewatkan ini dan Anda akan menghabiskan berjam-jam men-debug mengapa data Anda basi.
// BEFORE: Pages Router
export async function getServerSideProps() {
const data = await prisma.post.findMany()
return { props: { posts: data } }
}
export default function Blog({ posts }) { ... }
// AFTER: App Router Server Component
// app/[locale]/(pages)/blog/page.tsx
export default async function Blog() {
const posts = await prisma.post.findMany() // direct DB call — no API route needed
return <PostList posts={posts} />
}
// For fresh data on every request:
const data = await fetch('/api/posts', { cache: 'no-store' })
// For ISR-style revalidation:
const data = await fetch('/api/posts', { next: { revalidate: 60 } })
// useRouter migration:
// BEFORE (Pages Router)
import { useRouter } from 'next/router'
const router = useRouter()
const { slug } = router.query
// AFTER (App Router)
import { useRouter, useSearchParams } from 'next/navigation'
const searchParams = useSearchParams()
const slug = searchParams.get('slug')Anda tidak harus menulis ulang semuanya sekaligus. Next.js mendukung menjalankan Pages Router dan App Router berdampingan selama migrasi. Pendekatan WorkOS — yang sangat berharga bagi saya — adalah menguji seluruh versi App Router di balik feature flag sebelum beralih traffic produksi. Kami mulai dengan halaman yang paling tidak interaktif (landing page marketing, blog), konversi ke Server Components, validasi, lalu pindah ke halaman dashboard terautentikasi.
Kritis: Jangan pernah mengandalkan middleware Next.js sebagai satu-satunya gate otorisasi. CVE-2025-29927 mengungkap bahwa middleware bisa dilewati dengan memanipulasi header x-middleware-subrequest. Logika otorisasi aktual Anda harus berada di Server Components atau API routes — middleware untuk keputusan routing dan deteksi locale, bukan penegakan keamanan. Selalu verifikasi sesi/izin server-side di dalam route handler itu sendiri.
Hari ini, semua proyek baru di Commsult dimulai dengan App Router. Manfaatnya berlipat ganda: Streaming berarti pengguna melihat konten secara progresif daripada menunggu fetch data paling lambat. Server Components berarti klien Prisma tidak pernah dikirim ke browser. Nested layouts berarti state bersama (seperti sesi pengguna) mengalir ke bawah tanpa prop drilling. Untuk migrasi brownfield, saya melakukannya secara bertahap.
Sebelum memulai migrasi Pages Router ke App Router: (1) Audit semua library pihak ketiga untuk kompatibilitas SSR — periksa isu mereka untuk laporan 'use client' atau 'window is not defined'. (2) Petakan semua penggunaan getServerSideProps dan getStaticProps ke ekuivalen App Router. (3) Identifikasi semua halaman yang menggunakan useRouter untuk query param. (4) Rencanakan hierarki layout — root layout, locale layouts, dashboard layouts — sebelum menulis kode. (5) Siapkan App Router di bawah /app bersama /pages yang ada untuk menjalankan keduanya secara bersamaan selama migrasi. (6) Jangan pernah menaruh logika otorisasi hanya di middleware.