Migration Pages Router sang App Router trong Next.js 16: Playbook Từng Bước (2026)
Migration Pages Router sang App Router trong Next.js 16 là quy trình incremental 2-6 tuần với dự án cỡ vừa. Codemod tự động 80-90% thay đổi cơ học, còn lại là refactor data fetching. Playbook đầy đủ từ 5 dự án thực tế.
Migration từ Pages Router sang App Router trong Next.js 16 là một quy trình incremental (từng bước). Bạn giữ nguyên thư mục pages/ đang chạy production, tạo thư mục app/ song song, rồi chuyển từng route một trong khoảng 2–6 tuần với dự án cỡ vừa. Codemod chính thức @next/codemod tự động hóa khoảng 80–90% các thay đổi cơ học (async params, đổi import, chuyển API Routes), phần còn lại là refactor data-fetching từ getServerSideProps sang async Server Components. Bài này là playbook đầy đủ mà tôi đã dùng cho ba team migration trong năm qua.
Pages Router chưa bị deprecate tính đến Next.js 16. Không có timeline gỡ bỏ chính thức, nhưng feature mới chỉ được thêm vào App Router.
Chạy lệnh npx @next/codemod@canary upgrade latest để tự động chuyển async request APIs, đổi next/router thành next/navigation, và di chuyển API Routes.
Thư mục pages/ và app/có thể tồn tại song song trong cùng một dự án, cho phép migrate từng route một mà không cần big-bang rewrite.
getServerSideProps được thay bằng async Server Components; getStaticPaths đổi thành generateStaticParams; _app.tsx cộng _document.tsx gộp vào app/layout.tsx.
Trong Next.js 16, middleware.ts được đổi tên thành proxy.ts. Codemod xử lý luôn khi bạn upgrade.
Ước tính effort: dự án nhỏ (<20 routes) 3–5 ngày, cỡ vừa (20–100 routes) 2–4 tuần, cỡ lớn (>100 routes) 6–12 tuần.
Pages Router có bị deprecate trong Next.js 16 không?
Câu trả lời ngắn: Không. Tính đến bản Next.js 16 (phát hành cuối 2025), Pages Router vẫn được hỗ trợ chính thức, vẫn có mặt trong docs, và vẫn nhận bug fix. Vercel chưa thông báo timeline gỡ bỏ và có lẽ sẽ không thông báo trong 2–3 năm tới. Đơn giản vì quá nhiều code production đang chạy trên nó.
Tuy nhiên, thực tế đằng sau con số đó khắc nghiệt hơn. Từ Next.js 13.4 (2023), tất cả feature mới chỉ được thêm vào App Router: Server Components, Server Actions, Partial Prerendering, Cache Components, React Compiler auto-memoization, streaming với Suspense, Turbopack builds tối ưu hơn. Pages Router giống trạng thái "duy trì" của AngularJS trước khi ngừng hỗ trợ hoàn toàn. Nó hoạt động, nhưng bạn đang trượt xa khỏi hệ sinh thái.
Đặc biệt trong Next.js 16, một breaking change ảnh hưởng cả hai router là middleware.ts được đổi tên thành proxy.ts. Nếu bạn đang lên plan upgrade lên Next 16, hãy đọc bài chuyển đổi middleware sang proxy trong Next.js 16 trước. Thay đổi đó độc lập với việc migrate router.
Bảng so sánh: Pages Router vs App Router
Trước khi bắt tay vào code, dưới đây là bảng đối chiếu mà tôi in ra dán cạnh bàn khi làm migration đầu tiên. Nó giúp cả team nói cùng một ngôn ngữ khi review PR.
Khía cạnh
Pages Router
App Router (Next.js 16)
Thư mục
pages/
app/
File route
pages/about.tsx
app/about/page.tsx
Loại component mặc định
Client Component (mọi thứ hydrate)
Server Component (opt-in 'use client')
Data fetching SSR
getServerSideProps
async function Page() + fetch()
Data fetching SSG
getStaticProps + getStaticPaths
generateStaticParams + fetch cache
API endpoints
pages/api/*.ts
app/api/*/route.ts (Route Handlers)
Global layout
_app.tsx + _document.tsx
app/layout.tsx (root layout)
Loading state
Không có convention
loading.tsx + Suspense
Error boundary
_error.tsx
error.tsx per-route
Router hook
useRouter từ next/router
useRouter từ next/navigation
Điểm khác biệt căn bản nhất không nằm ở file convention. Nó nằm ở model rendering. Pages Router chạy toàn bộ page trên cả server (pre-render) và client (hydrate). App Router chia rõ ranh giới: Server Components chạy chỉ trên server (không có JS payload gửi xuống browser), Client Components chạy trên cả hai. Chuyển đổi mindset này là phần khó hơn cả việc gõ code.
Checklist chuẩn bị trước khi migrate
Đừng chạy codemod trên nhánh main. Đây là các bước tôi luôn làm trước khi mở terminal để chạy migration đầu tiên:
Pin phiên bản toolchain: Node.js 20.9.0+ (Node 18 đã bị bỏ hỗ trợ trong Next 16), TypeScript 5.1+, npm 10+ hoặc pnpm 9+.
Bump Next.js lên bản mới nhất trong Pages Router trước:npm install next@latest react@latest react-dom@latest. Sửa hết lỗi build trước khi động vào App Router (không trộn hai loại lỗi lại với nhau).
Bật strict mode trong tsconfig.json: App Router bắt buộc async params, TypeScript sẽ giúp bạn tìm chỗ quên await.
Tạo nhánh migration riêng:git checkout -b migrate/app-router. Không squash merge cho tới khi CI xanh.
Chụp baseline metrics: Lighthouse LCP, bundle size, thời gian build. Sau migration bạn cần số liệu để chứng minh performance tốt hơn (hoặc rollback nếu tệ hơn).
Audit dependencies: Chạy npx npm-check-updates. Các package như next-auth, swr, @tanstack/react-query đã có phiên bản tương thích App Router, nâng cấp trước.
Chạy Codemod: tự động hóa 80% công việc
Next.js cung cấp codemod chính thức xử lý hầu hết các thay đổi cơ học. Đây là công cụ duy nhất bạn cần chạy đầu tiên:
# Chạy codemod upgrade - tự động bump dependencies + transform code
npx @next/codemod@canary upgrade latest
# Hoặc chạy từng transform riêng biệt để review dần
npx @next/codemod@latest next-async-request-api .
npx @next/codemod@latest next-request-geo-ip .
npx @next/codemod@latest built-in-next-font .
Chuyển sync params, searchParams, cookies(), headers(), draftMode() sang async.
Đổi tên middleware.ts thành proxy.ts và cập nhật config tương ứng.
Thay import { useRouter } from 'next/router' bằng import { useRouter } from 'next/navigation' trong các file đã ở dưới app/.
Chuyển getStaticPaths sang generateStaticParams.
Port pages/api/* sang app/api/*/route.ts khi được yêu cầu.
Sau khi chạy xong, luôn commit ngay với message chore: run @next/codemod upgrade để review dễ hơn. Đừng edit đè lên diff của codemod trong cùng commit, vì bạn sẽ mất khả năng phân biệt lỗi do codemod hay do bạn tự sửa.
Codemod đạt tỉ lệ thành công khoảng 90% cho dự án chuẩn, giảm dần với business logic phức tạp. Việc còn lại (bao gồm refactor data fetching, xử lý auth logic tự custom, và điều chỉnh các loader Webpack không tương thích Turbopack) vẫn cần bạn ngồi làm bằng tay.
Migrate _app.tsx và _document.tsx sang app/layout.tsx
Trong Pages Router, bạn có hai file config toàn cục: _document.tsx control <html>/<head>, và _app.tsx wrap providers. App Router gộp cả hai vào một file duy nhất: app/layout.tsx.
Providers từ _app.tsx (Redux, TanStack Query, Auth context) cần được extract vào một Client Component riêng vì chúng dùng React context, không chạy được trên Server Component:
// app/providers.tsx - Client boundary cho tất cả context providers
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { SessionProvider } from 'next-auth/react';
import { useState } from 'react';
export function Providers({ children }: { children: React.ReactNode }) {
const [queryClient] = useState(() => new QueryClient());
return (
<SessionProvider>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</SessionProvider>
);
}
// app/layout.tsx - import và wrap
import { Providers } from './providers';
export default function RootLayout({ children }) {
return (
<html lang="vi">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}
Metadata (title, description, Open Graph) không còn dùng next/head. Thay vào đó bạn export const metadata hoặc function generateMetadata từ mỗi page.tsx hoặc layout.tsx. Đây là một trong những cải tiến DX rõ nét nhất. Không còn cảnh gõ <Head> lung tung khắp components con.
getServerSideProps thay thế bằng gì trong App Router?
Đây là câu hỏi tôi nghe nhiều nhất khi tư vấn migration. Câu trả lời: getServerSideProps được thay bằng async Server Components. Bạn fetch data trực tiếp trong component, không cần export function riêng.
// app/products/[id]/page.tsx - Server Component với async
type PageProps = {
params: Promise<{ id: string }>;
};
export default async function ProductPage({ params }: PageProps) {
// params là Promise trong Next.js 15+, bắt buộc await
const { id } = await params;
// fetch chạy trên server, không gửi API key xuống client
const res = await fetch(`https://api.example.com/products/${id}`, {
cache: 'no-store', // tương đương getServerSideProps: luôn fresh
});
const product: Product = await res.json();
return <h1>{product.name}</h1>;
}
Ba khác biệt quan trọng cần nắm:
params giờ là Promise: Đây là thay đổi Next.js 15+ để framework có thể stream static shell trước khi dynamic segments resolve. Quên await là lỗi phổ biến nhất khi upgrade (may mắn là TypeScript sẽ báo).
Cache semantics khác: Trong Next.js 16 với Cache Components, mặc định fetch không cache. Bạn phải opt-in bằng { cache: 'force-cache' } hoặc dùng directive use cache trong Next.js 16.
Không cần props drilling: Server Component có thể await trực tiếp, không cần return { props } rồi truyền xuống.
Với getStaticProps cộng getStaticPaths, mapping tương đương là function generateStaticParams:
Trong App Router, hook useRouter được import từ next/navigation, không phải next/router. API cũng thay đổi đáng kể, nhiều property bị chia thành các hook riêng.
Quy tắc vàng: bất kỳ component nào import từ next/navigation đều phải là Client Component, thêm 'use client' ở đầu file. Nếu bạn chỉ cần đọc pathname/searchParams cho Server Component, dùng props searchParams được inject vào page.tsx thay vì hook.
Chuyển pages/api sang Route Handlers
API Routes cũ (pages/api/*.ts) được thay bằng Route Handlers (app/api/*/route.ts). Signature hoàn toàn khác. Không còn NextApiRequest/NextApiResponse, thay bằng Web standard Request/Response (theo Web Fetch API).
// Trước - pages/api/users/[id].ts
import type { NextApiRequest, NextApiResponse } from 'next';
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
const user = await db.user.findUnique({ where: { id: Number(req.query.id) } });
return res.status(200).json(user);
}
res.setHeader('Allow', ['GET']);
res.status(405).end();
}
// Sau - app/api/users/[id]/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function GET(
_req: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const { id } = await params;
const user = await db.user.findUnique({ where: { id: Number(id) } });
return NextResponse.json(user);
}
Route Handlers hỗ trợ đầy đủ HTTP methods qua named exports (GET, POST, PUT, DELETE, PATCH). Không cần if (req.method === '...') nữa. Mỗi method là một function riêng.
Với các endpoint chỉ dùng nội bộ (form submission, mutations), cân nhắc chuyển sang Server Actions thay vì Route Handlers. Tôi đã viết bài chi tiết so sánh Route Handlers với Server Actions giúp bạn quyết định chọn cái nào cho từng use case.
Coexistence: chạy Pages và App Router song song
Điểm mạnh nhất của cách tiếp cận incremental là pages/ và app/ có thể cùng tồn tại. Next.js sẽ ưu tiên app/ nếu cả hai đều có route trùng, cho phép bạn chuyển từng URL một mà không downtime.
my-app/
├── app/
│ ├── layout.tsx # Root layout mới
│ ├── page.tsx # Home page đã migrate
│ └── products/
│ └── [id]/
│ └── page.tsx # /products/[id] đã migrate
└── pages/
├── _app.tsx # Vẫn dùng cho các route chưa migrate
├── about.tsx # /about vẫn chạy Pages Router
├── contact.tsx # /contact vẫn chạy Pages Router
└── api/
└── legacy-webhook.ts # API cũ chưa cần chuyển
Chiến lược tôi thường áp dụng: migrate theo traffic ngược từ thấp lên cao. Bắt đầu với các route ít user (trang cài đặt, admin), tích lũy confidence, rồi mới đụng đến homepage và product pages. Nếu có bug regression, blast radius nhỏ.
Một thứ cần lưu ý: link giữa app/ và pages/ sẽ trigger full page navigation (hard reload), không phải client-side transition. Đó là lý do bạn muốn migrate các flow liên kết chặt (như checkout: cart, payment, confirmation) cùng một lượt thay vì rải rác.
Ước tính effort: mất bao lâu để migrate?
Đây là estimate tôi đưa cho stakeholder khi scoping migration, dựa trên 5 dự án thực tế đã làm 2024–2026:
Các activity chiếm nhiều thời gian nhất (từ retro của các team tôi làm việc cùng):
Refactor data fetching (35–40% thời gian): Chuyển từ getServerSideProps sang Server Components cộng fetch() yêu cầu suy nghĩ lại về cache strategy.
Auth và session management (15–20%):next-auth có bản App Router, nhưng middleware auth logic cần rewrite.
Custom Webpack loaders (10–15%): Turbopack (mặc định trong Next 16) chưa support 100% Webpack loaders. Phải thay thế hoặc fallback --webpack.
Testing (15%): Server Components khó test hơn, cần setup Jest/Vitest riêng cho async components.
Documentation và training team (10%): Đừng bỏ qua. Mental model mới cần thời gian để thấm.
Để giảm downside risk, tôi luôn khuyến nghị team dùng streaming với Suspense và loading.tsx ngay từ những route đầu tiên migrate. Nó cải thiện TTFB rõ rệt và cho stakeholder thấy giá trị migration bằng số liệu Core Web Vitals cụ thể, không chỉ là "code sạch hơn".
Câu hỏi thường gặp
Có nên migrate từ Pages Router sang App Router năm 2026 không?
Nếu app của bạn đang chạy production ổn định và không cần feature mới của React 19 (Server Components, Server Actions, PPR), bạn có thể đợi thêm 6–12 tháng. Nhưng cho tất cả dự án greenfield hoặc app có roadmap dài hạn, migrate ngay. Vercel sẽ không thêm feature mới cho Pages Router nữa.
Codemod của Next.js có tự động chuyển hết code không?
Không hoàn toàn. Codemod chính thức đạt tỉ lệ thành công khoảng 90% cho các thay đổi cơ học (async params, đổi imports, đổi tên file). Data fetching logic, custom auth middleware, và Webpack loaders vẫn cần bạn refactor thủ công. Codemod cung cấp baseline đúng, phần còn lại là fix các runtime error.
Có phải migrate hết một lượt hay có thể làm dần?
Làm dần. Thư mục pages/ và app/ có thể tồn tại song song trong cùng dự án Next.js. Bạn chuyển từng route (hoặc từng cụm route liên quan) một, deploy lên production, verify, rồi chuyển tiếp. Không cần big-bang rewrite.
useRouter trong App Router import từ đâu?
Import từ next/navigation, không phải next/router. Component dùng hook này bắt buộc phải là Client Component (khai báo 'use client' ở đầu file). API cũng khác: router.pathname đổi thành usePathname(), router.query đổi thành useSearchParams().
Server Component có replace hết getServerSideProps không?
Có, với cách viết đơn giản hơn. Bạn khai báo async function Page() và fetch() data trực tiếp bên trong, không cần export function riêng. Lưu ý cache semantics trong Next.js 16 khác: mặc định fetch không cache, phải opt-in với { cache: 'force-cache' } hoặc directive 'use cache'.
Migration từ Webpack sang Turbopack production build trong Next.js 16 với config chi tiết, custom loaders (SVGR, MDX, GraphQL), benchmark thực tế trên 3 codebase, và checklist migration từng bước.
Hướng dẫn setup instrumentation.ts trong Next.js 16 để tích hợp OpenTelemetry, tracing và error tracking. Bao gồm @vercel/otel, sdk-node, sampling production và onRequestError với Sentry.
Hướng dẫn dùng Suspense và loading.tsx trong Next.js 16 để stream HTML theo chunks, giảm TTFB xuống dưới 200ms, và tối ưu Core Web Vitals với React 19 use() hook cùng preload pattern.