Skip to main content
BlogWeb Dev

Migrasi Next.js App Router: Pelajaran dari Proyek Nyata

Pelajaran berharga dari migrasi aplikasi Next.js produksi dari Pages Router ke App Router — apa yang rusak, apa yang berhasil, dan apa yang akan saya lakukan berbeda.

Matthews Wong

Software Engineer

Juni 202510 menit baca
Web Dev

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? Memigrasikan aplikasi web produksi dari Pages Router ke App Router bisa menjadi salah satu pengalaman paling edukatif (kadang menyakitkan) bagi seorang pengembang. Inilah yang perlu diketahui sebelum Anda mulai.

Mengapa Harus Migrasi?

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.

Pergeseran Model Mental

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.

Apa yang Sebenarnya Rusak Selama Migrasi

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.

Menangani Transisi Data Fetching

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')

Strategi Migrasi Bertahap

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.

Yang Saya Gunakan dan Mengapa

Hari ini, proyek baru sebaiknya 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, lakukan secara bertahap.

Checklist Migrasi

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.

Sumber & Bacaan Lanjut

Artikel Terkait

FAQ

Pertanyaan Umum

Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.

Apa pergeseran model mental terbesar saat migrasi dari Pages Router ke App Router?

Pergeseran utamanya adalah 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, dan Anda harus menandai komponen dengan 'use client' begitu Anda menambahkan useState, useEffect, atau API browser apa pun.

Bagaimana cara menghindari penyebaran 'use client' yang tidak perlu?

Sebelum menyentuh satu file pun, audit setiap komponen untuk useState, useEffect, dan event handler. Tandai dengan warna — hijau untuk render murni (aman sebagai Server Component), kuning untuk hook data saja, merah untuk interaksi browser. Ini mencegah kesalahan umum menandai seluruh subtree komponen sebagai klien padahal hanya satu komponen daun yang benar-benar butuh akses browser.

Apa kerusakan paling umum selama migrasi Pages Router ke App Router?

Tiga masalah paling banyak menyebabkan gesekan: pertama, library pihak ketiga yang mengakses window atau document langsung (library chart, rich text editor, date picker) akan error di server dan membutuhkan dynamic() dengan ssr: false. Kedua, useRouter dari next/router dan useRouter dari next/navigation berbeda perilaku meski namanya sama — versi App Router tidak bisa membaca query param sehingga butuh useSearchParams() secara terpisah. Ketiga, API route pindah dari pages/api/ ke app/api/[...route]/route.ts dengan signature ekspor yang sama sekali berbeda.

Mengapa berbahaya menaruh logika otorisasi hanya di middleware Next.js?

CVE-2025-29927 mengungkap bahwa middleware Next.js bisa dilewati dengan memanipulasi header x-middleware-subrequest. Middleware seharusnya hanya menangani keputusan routing dan deteksi locale. Pengecekan sesi dan izin yang sesungguhnya harus selalu ditegakkan di sisi server di dalam route handler atau Server Component itu sendiri.

Apa strategi yang direkomendasikan untuk migrasi aplikasi produksi secara bertahap?

Next.js mendukung menjalankan Pages Router dan App Router berdampingan, sehingga Anda tidak perlu menulis ulang semuanya sekaligus. Mulai dari halaman yang paling tidak interaktif seperti landing page marketing dan blog, konversi ke Server Components, validasi, lalu pindah ke halaman dashboard terautentikasi. Menguji seluruh versi App Router di balik feature flag sebelum beralih traffic produksi — pendekatan yang didokumentasikan WorkOS — membantu memastikan migrasi tanpa downtime.