Migrare de la Pages Router la App Router în Next.js 16: Playbook Complet cu Estimări

Playbook practic de migrare de la Pages Router la App Router în Next.js 16: pași, estimări realiste (8-14 zile), tabel de echivalențe și exemple de cod din producție.

Migrare Pages la App Router: Next.js 16

Actualizat: 10 septembrie 2026

Migrarea de la Pages Router la App Router în Next.js 16 este un proces incremental care înlocuiește pages/ cu app/, convertește getServerSideProps și getStaticProps în Server Components asincrone, transformă _app.tsx și _document.tsx într-un layout, iar rutele API se mută în Route Handlers. Am migrat șase echipe de producție de la Pages Router în ultimele optsprezece luni. Din experiența mea, o aplicație de complexitate medie (30–60 de pagini) cere între 8 și 14 zile de lucru concentrat, cu Pages Router și App Router rulând simultan pe parcursul tranziției.

  • App Router și Pages Router coexistă în aceeași aplicație, deci poți migra o rută pe zi fără să oprești producția.
  • getServerSideProps devine cod scris direct în componenta async function Page(), iar getStaticProps se traduce în fetch() cu cache: 'force-cache'.
  • Fișierele _app.tsx și _document.tsx se combină într-un singur app/layout.tsx, care conține tag-urile <html> și <body>.
  • Rutele API din pages/api/ se mută în app/api/*/route.ts, cu exporturi numite (GET, POST) în loc de export default.
  • Meta tag-urile din next/head se înlocuiesc cu Metadata API, static sau dinamic prin generateMetadata.
  • Estimare realistă: 8–14 zile pentru 30–60 de pagini. Adaugă 3–5 zile pentru fiecare integrare complexă (auth, i18n, tema).

Care este diferența dintre Pages Router și App Router?

Pages Router (introdus în 2016) este sistemul de rutare bazat pe fișiere din directorul pages/, în care fiecare fișier .tsx este o rută, iar datele se preîncarcă prin funcțiile speciale getServerSideProps, getStaticProps și getStaticPaths. Totul este, implicit, o Client Component. Adică se rulează în browser și îngroașă bundle-ul de JavaScript.

App Router (stabil din Next.js 13.4, matur în Next.js 16) folosește directorul app/, în care fișierele au roluri semantice: page.tsx pentru rută, layout.tsx pentru shell-uri partajate, loading.tsx pentru Suspense boundaries, error.tsx pentru error boundaries. Componentele sunt Server Components by default, ceea ce înseamnă că fetch() se face pe server, secretele rămân pe server, iar HTML-ul ajunge deja randat la client.

În aceeași aplicație, ambele routere coexistă pașnic. Dacă ai o rută /dashboard în pages/dashboard.tsx și creezi app/dashboard/page.tsx, App Router are prioritate. Asta e mecanica care face migrarea sigură: ștergi fișierul din pages/ doar după ce echivalentul din app/ funcționează în producție.

Cât durează migrarea de la Pages Router la App Router?

În experiența mea de migrare a șase echipe, estimarea depinde brutal de trei factori: numărul de pagini, complexitatea layout-urilor globale și adâncimea integrărilor terțe (auth, i18n, feature flags, analytics).

  • Aplicație mică (10–25 pagini, un layout global): 4–7 zile de lucru pentru un singur developer senior.
  • Aplicație medie (30–60 pagini, dashboard cu autentificare): 8–14 zile, sau 2 sprint-uri cu doi developeri.
  • Aplicație mare (100+ pagini, i18n, A/B testing, CMS custom): 4–8 săptămâni pentru o echipă de trei.

Adaugă un buffer de 20% pentru edge cases neanticipate: biblioteci care rulează doar în client (Framer Motion, react-dnd), integrări cu getInitialProps depreciat, sau layout-uri care depind de context providers încurcați. Am migrat un dashboard SaaS de 45 de rute în 11 zile lucrătoare, dar unul cu i18n bazat pe next-i18next a durat 19 zile fiindcă a trebuit să înlocuim biblioteca cu next-intl.

Tabel de echivalențe: Pages Router vs App Router

Ține deschis acest tabel pe al doilea monitor pe parcursul migrării. Este cel mai des consultat cheat sheet din procesul meu:

ConceptPages RouterApp Router (Next.js 16)
Directorul de rutepages/app/
Fișierul ruteipages/about.tsxapp/about/page.tsx
Layout globalpages/_app.tsx + _document.tsxapp/layout.tsx
Data fetching (SSR)getServerSidePropsasync function Page() + fetch()
Data fetching (SSG)getStaticPropsfetch(url, { cache: 'force-cache' })
Parametri dinamicigetStaticPathsgenerateStaticParams
Rute APIpages/api/user.tsapp/api/user/route.ts
Meta tag-uri<Head> din next/headexport const metadata
Componentă implicităClient ComponentServer Component
Formulare mutațiiPOST către pages/api/Server Actions

Migrare pas cu pas: procesul în șapte etape

Sincer, iată ordinea pe care o urmez la fiecare migrare, testată pe șase proiecte de producție. Nu sări peste etape, chiar dacă pare tentant. Ordinea previne conflicte de rutare și minimizează durata reglării fine.

1. Ridică baseline-ul: actualizează la Next.js 16

Înainte să atingi app/, urcă la Next.js 16 pe Pages Router-ul existent. Rezolvă tot ce se strică: deprecări React 19, Turbopack activat, tipuri noi. Rulează testele. Deployează. Doar apoi treci mai departe.

npm install next@16 react@19 react-dom@19
npm install -D @types/react@19 @types/react-dom@19

2. Creează app/layout.tsx minimal

Adaugă un app/layout.tsx care conține doar tag-urile HTML esențiale. Asta activează App Router fără să impacteze rutele existente:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ro">
      <body>{children}</body>
    </html>
  )
}

3. Migrează o rută pilot

Alege o rută simplă, fără dependențe: /about, /contact, sau /terms. Creează app/about/page.tsx, mută conținutul, șterge pages/about.tsx. Testează în producție. Rezolvă surprizele.

4. Portează utilitățile partajate

Context providers, teme, analytics: mută-le în app/providers.tsx (un Client Component wrapper) și include în layout. Aici trebuie să separi cu grijă cod client de cod server.

5. Migrează rutele statice

Paginile fără data fetching, sau cele cu getStaticProps simplu, sunt câștigurile ușoare. Lasă rutele autentificate și pe cele cu SSR complex la sfârșit.

6. Migrează rutele dinamice și API-urile

Aici ordinea contează: migrează API-urile înainte de rutele care le apelează, dacă schimbi și schema. Altfel, migrează în ordine inversă (rute apoi API-uri) pentru izolare.

7. Șterge dependințele Pages Router

Când pages/ conține doar _app.tsx și _document.tsx, elimină și pe acestea. Șterge getInitialProps, orice referință la next/router înlocuită de next/navigation.

Migrarea getServerSideProps și getStaticProps

Cea mai frecventă întrebare pe care o primesc: „unde se duce getServerSideProps?" Răspunsul scurt: dispare. Codul din interior se mută direct în corpul componentei, care devine async.

Înainte (Pages Router)

// pages/products/[id].tsx
import type { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const res = await fetch(`https://api.example.com/products/${ctx.params?.id}`)
  const product = await res.json()
  return { props: { product } }
}

export default function ProductPage({ product }) {
  return <h1>{product.name}</h1>
}

După (App Router)

// app/products/[id]/page.tsx
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  const res = await fetch(`https://api.example.com/products/${id}`, {
    cache: 'no-store', // echivalentul comportamentului SSR
  })
  const product = await res.json()

  return <h1>{product.name}</h1>
}

Pentru getStaticProps, înlocuiește cache: 'no-store' cu cache: 'force-cache' (implicit în Next.js 16 pentru fetch în build). Pentru getStaticPaths, folosește generateStaticParams pentru ISR și rute dinamice. Sintaxa e similară, dar rulează pe rutele din app/.

Migrarea _app.tsx și _document.tsx în layout-uri

Cele două fișiere globale din Pages Router se combină într-unul singur în App Router. Iată cum arată tranziția:

Configurația veche

// pages/_document.tsx
import { Html, Head, Main, NextScript } from 'next/document'
export default function Document() {
  return (
    <Html lang="ro">
      <Head><link rel="icon" href="/favicon.ico" /></Head>
      <body><Main /><NextScript /></body>
    </Html>
  )
}

// pages/_app.tsx
import { ThemeProvider } from 'next-themes'
import '../styles/globals.css'

export default function App({ Component, pageProps }) {
  return (
    <ThemeProvider>
      <Component {...pageProps} />
    </ThemeProvider>
  )
}

Configurația nouă

// app/layout.tsx (Server Component)
import { Providers } from './providers'
import './globals.css'

export const metadata = {
  icons: { icon: '/favicon.ico' },
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ro">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  )
}

// app/providers.tsx (Client Component)
'use client'
import { ThemeProvider } from 'next-themes'

export function Providers({ children }: { children: React.ReactNode }) {
  return <ThemeProvider>{children}</ThemeProvider>
}

Separarea în două fișiere nu e stilistică, e obligatorie. Layout-ul rulează pe server, iar ThemeProvider folosește useState intern, așa că are nevoie de 'use client'. Pattern-ul e atât de universal încât îl scriu automat la începutul fiecărei migrări. Dacă folosești și autentificare, aruncă un ochi pe ghidul de Auth.js v5 în Next.js 16 cu sesiuni și roluri înainte să pui providerii.

Cum convertești rutele API în Route Handlers?

Rutele din pages/api/*.ts devin app/api/*/route.ts, cu exporturi numite pentru fiecare metodă HTTP. Nu mai există export default (req, res) =>. Consultă ghidul complet Route Handlers pentru API REST pentru pattern-uri avansate.

Înainte

// 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: req.query.id } })
    return res.status(200).json(user)
  }
  if (req.method === 'DELETE') {
    await db.user.delete({ where: { id: req.query.id } })
    return res.status(204).end()
  }
  res.setHeader('Allow', ['GET', 'DELETE'])
  return res.status(405).end()
}

După

// app/api/users/[id]/route.ts
import { NextResponse } from 'next/server'

export async function GET(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  const user = await db.user.findUnique({ where: { id } })
  return NextResponse.json(user)
}

export async function DELETE(
  req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params
  await db.user.delete({ where: { id } })
  return new NextResponse(null, { status: 204 })
}

Codul devine mai declarativ. Fiecare metodă HTTP este o funcție separată, semnătura este Web Fetch standard (Request, Response), și nu mai există branch-uire manuală pe req.method. Cei care vin din Remix sau Hono se vor simți acasă.

Migrarea next/head în Metadata API

Tag-urile <Head> din next/head se înlocuiesc cu Metadata API static (obiect exportat) sau dinamic (funcție generateMetadata). API-ul suportă native Open Graph, Twitter cards, canonical URLs, alternates i18n, robots. Practic tot ce configurai manual.

Metadata static

// app/blog/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Blog | Numele Site-ului',
  description: 'Articole despre Next.js și React.',
  openGraph: {
    title: 'Blog',
    images: ['/og-blog.png'],
  },
}

Metadata dinamic

// app/products/[slug]/page.tsx
import type { Metadata } from 'next'

export async function generateMetadata({
  params,
}: {
  params: Promise<{ slug: string }>
}): Promise<Metadata> {
  const { slug } = await params
  const product = await fetch(`/api/products/${slug}`).then(r => r.json())

  return {
    title: product.name,
    description: product.description,
    openGraph: { images: [product.imageUrl] },
  }
}

Consultă documentația oficială pentru generateMetadata pentru câmpuri avansate: alternates.canonical, robots, verification, manifest. Pentru contextul mai larg al schimbărilor de rendering din 16, merită și specificația Server Components de la echipa React. Nu mai ai nevoie de next/head în App Router. E chiar interzis într-un Server Component.

Migrezi incremental sau tot deodată?

Incremental. Întotdeauna. În cinci din șase migrări pe care le-am făcut, echipa a încercat inițial abordarea „big bang" (branch lung, tot deodată), și în trei din cinci am oprit-o și am refăcut incremental după prima săptămână de conflicte de merge.

Abordarea incrementală funcționează pentru că App Router și Pages Router coexistă. Poți avea /dashboard în App Router și /settings în Pages Router în același build. Livrezi câte o rută pe deploy, monitorizezi metricele Real User Monitoring, faci rollback dacă ceva se rupe.

Ordinea mea preferată: (1) rute statice publice (about, pricing), (2) landing pages (home, categorii), (3) rute autentificate simple, (4) dashboard-uri complexe, (5) rute API, (6) ștergere pages/. Așa reduci riscul pe fluxurile care generează venit.

Capcane frecvente și cum le eviți

După șase migrări, aceleași cinci probleme apar de fiecare dată. Le enumăr ca să le eviți încă din prima săptămână.

1. „use client" viral

Când o componentă are 'use client', tot arborele importat devine client. Am văzut echipe care puneau 'use client' pe layout și pierdeau avantajul Server Components. Regulă simplă: pune 'use client' cât mai jos posibil în arbore, pe frunze (butoane cu onClick, formulare cu useState).

2. Uitarea că params este Promise

În Next.js 16, params și searchParams sunt Promise. Accesul direct params.id (fără await) generează warning-uri acum și va deveni eroare hard. Folosește const { id } = await params.

3. Import greșit al router-ului

next/router nu funcționează în App Router. Trebuie next/navigation: useRouter, usePathname, useSearchParams. API-ul e diferit; router.push('/x') merge, dar router.query nu există.

4. Cookie-uri și headere

În Pages Router accesai cookie-urile prin ctx.req.cookies. În App Router folosești cookies() din next/headers, iar în Next.js 16 e async. Similar pentru headers().

5. Middleware devenit proxy

În Next.js 16, middleware.ts a fost redenumit proxy.ts pentru claritate. Vezi ghidul complet de migrare de la middleware la proxy.ts. Nu uita să redenumești fișierul și să actualizezi importurile. Funcțiile NextRequest și NextResponse rămân la fel.

Întrebări frecvente

Trebuie să migrez de la Pages Router la App Router?

Nu obligatoriu. Pages Router este încă suportat oficial în Next.js 16 și nu are dată anunțată de deprecare. Migrează dacă ai nevoie de Server Components, Server Actions, layout-uri partajate cu state persistent, sau streaming granular. Pentru aplicații mici cu SSR simplu, Pages Router e perfect funcțional.

Pot rula Pages Router și App Router în același timp?

Da, coexistă în aceeași aplicație. Dacă ai o rută atât în pages/ cât și în app/, App Router are prioritate. Asta face migrarea incrementală sigură: muți câte o rută pe deploy și verifici în producție înainte să ștergi versiunea veche.

Ce se întâmplă cu getInitialProps în App Router?

getInitialProps nu este suportat în App Router. Trebuie să rescrii logica ca async function Page() cu fetch, sau, dacă rulai cod client-side inițial, să folosești useEffect într-o Client Component. În Pages Router rămâne funcțional, dar deja este marcat legacy.

Sunt codemod-urile oficiale suficiente pentru migrare?

Nu. Codemod-urile automatizează transformări sintactice (imports, redenumiri de props), dar nu convertesc arhitectura. Trebuie să decizi manual ce e Server Component, ce e Client Component, cum se împart layout-urile, cum se migrează data fetching. Economisesc timp, nu iau decizii.

Cât efect are migrarea asupra SEO?

Neutru sau pozitiv dacă păstrezi URL-urile identice și implementezi Metadata API corect. App Router livrează HTML mai rapid (Server Components streaming), ceea ce îmbunătățește Largest Contentful Paint. Cel mai mare risc SEO: URL-urile care se schimbă accidental. Verifică fiecare rută migrată cu curl -I înainte de deploy.

Jasmine Patel
Despre Autor Jasmine Patel

Web framework specialist comparing Next.js to everything else so you don't have to. Migrates teams off legacy stacks for fun.