Bỏ qua đến nội dung chính
Next.jsApp RouterReactmigrationfrontend

Next.js App Router: Migrate Từ Pages Router Thế Nào?

Next.js App Router migrate từ Pages Router: file convention mới, layout, loading, route handler, server component. Plan từng bước an toàn.

Xuất bản 8 phút đọc

Next.js App Router đã production-ready từ 13.4. Bài này hướng dẫn migrate từ Pages Router an toàn — file convention, data fetching, API route, metadata.

Pages vs App Router cùng tồn tại

Trong cùng project có thể có cả pages/app/. Next route ưu tiên app/ nếu trùng path. Cho phép migrate từng route — không phải big bang rewrite.

File convention mới

Pages RouterApp RouterVai trò
pages/index.tsxapp/page.tsxRoute /
pages/blog/[slug].tsxapp/blog/[slug]/page.tsxDynamic route
pages/_app.tsxapp/layout.tsxWrapper toàn app
pages/api/users.tsapp/api/users/route.tsAPI endpoint
(không có)app/loading.tsxLoading UI
pages/_error.tsxapp/error.tsxError boundary
pages/_document.tsxapp/layout.tsx (root)HTML structure

Root layout

// app/layout.tsx — replace _app.tsx + _document.tsx
import { ReactNode } from 'react'
import { ThemeProvider } from '@/components/theme-provider'
import './globals.css'

export const metadata = {
  title: { default: 'Alodev', template: '%s | Alodev' },
  description: 'Web/app development',
}

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="vi">
      <body>
        <ThemeProvider>{children}</ThemeProvider>
      </body>
    </html>
  )
}

Data fetching: bỏ getServerSideProps

// ❌ Pages Router cũ
export async function getServerSideProps({ params }) {
  const post = await fetch(`https://api/posts/${params.slug}`).then(r => r.json())
  return { props: { post } }
}
export default function Post({ post }) { return <h1>{post.title}</h1> }

// ✓ App Router — async server component
export default async function Post({ params }) {
  const post = await fetch(`https://api/posts/${params.slug}`, {
    next: { revalidate: 60 }  // ISR-style cache
  }).then(r => r.json())
  return <h1>{post.title}</h1>
}

Cache options:

  • { cache: 'force-cache' } — static, default
  • { cache: 'no-store' } — dynamic, mỗi request
  • { next: { revalidate: 60 } } — ISR 60s
  • { next: { tags: ['posts'] } } — tag để revalidateTag

generateStaticParams

// ❌ Cũ: getStaticPaths
export async function getStaticPaths() {
  const posts = await db.posts.findAll()
  return { paths: posts.map(p => ({ params: { slug: p.slug } })), fallback: false }
}

// ✓ Mới
export async function generateStaticParams() {
  const posts = await db.posts.findAll()
  return posts.map(p => ({ slug: p.slug }))
}

export default async function Page({ params }) {
  const post = await db.posts.findBySlug(params.slug)
  return <article>{post.content}</article>
}

Metadata API thay Head

// ❌ Cũ: next/head
import Head from 'next/head'
export default function Page() {
  return (<>
    <Head>
      <title>Blog</title>
      <meta name="description" content="..." />
    </Head>
    <h1>Blog</h1>
  </>)
}

// ✓ Mới: declarative metadata
export const metadata = {
  title: 'Blog',
  description: 'Bài viết về web/app',
  openGraph: { title: 'Blog', images: ['/og.png'] },
}

export default function Page() { return <h1>Blog</h1> }

// Hoặc dynamic
export async function generateMetadata({ params }) {
  const post = await db.posts.findBySlug(params.slug)
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: { images: [post.cover] },
  }
}

API Route Handler

// ❌ Cũ: pages/api/users.ts
export default function handler(req, res) {
  if (req.method === 'POST') { /* ... */ }
}

// ✓ Mới: app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'

export async function GET(req: NextRequest) {
  const users = await db.users.findAll()
  return NextResponse.json(users)
}

export async function POST(req: NextRequest) {
  const body = await req.json()
  const user = await db.users.create(body)
  return NextResponse.json(user, { status: 201 })
}

Loading + Error UI

// app/blog/loading.tsx — auto Suspense fallback
export default function Loading() {
  return <div className="skeleton">Loading posts...</div>
}

// app/blog/error.tsx — auto error boundary
'use client'
export default function Error({ error, reset }) {
  return (
    <div>
      <h2>Có lỗi: {error.message}</h2>
      <button onClick={reset}>Thử lại</button>
    </div>
  )
}

Convention thay code — không cần wrap manual Suspense/ErrorBoundary.

Plan migrate từng bước

  1. Tuần 1: Setup app/ folder, tạo root layout cơ bản. Pages vẫn chạy.
  2. Tuần 2: Migrate 1 route ít quan trọng (about, terms). Test kỹ.
  3. Tuần 3-4: Migrate route content (blog, docs) — server component thắng nhiều.
  4. Tuần 5+: Migrate route phức tạp (dashboard, auth) — cẩn thận với client component, hooks library.
  5. Cuối: Migrate API routes (cùng path khác file).
  6. Done: Xoá pages/ khi không còn route nào.

Kết luận

Next.js App Router migration không quá đáng sợ — convention rõ, từng bước. Lợi ích: bundle nhỏ hơn, server fetch trực tiếp, streaming UX tốt. Đầu tư 2-4 tuần migrate cho project < 50 route là hợp lý. Đọc React Server Components để hiểu sâu paradigm mới.

Zalo