Next.js
Migración de Pages Router a App Router
Guía una migración gradual de Pages Router a App Router preservando datos, autenticación, metadata, caché, navegación, errores y métricas.
- Última actualización
- Actualizada
- Nivel
- Profundización
Next.js
Guía una migración gradual de Pages Router a App Router preservando datos, autenticación, metadata, caché, navegación, errores y métricas.
Migrar de Pages Router a App Router no consiste en mover archivos. Cambia el modelo de rendering, datos, layouts, metadata, navegación, caché y boundaries. La estrategia segura es migrar verticales completas, conservar comportamiento observable y medir cada cambio antes de retirar el sistema anterior.
inventory current route
→ identify behavior and contracts
→ create equivalent app route
→ move data and server/client boundaries
→ migrate metadata/loading/error
→ test hard and soft navigation
→ compare production metrics
→ remove old routeNo migres toda la carpeta de una vez. pages y app pueden coexistir mientras las URLs no colisionen.
Inventaría por ruta:
next/head y metadata.Registra métricas base: TTFB, LCP, bundle cliente, errores y conversión.
| Pages Router | App Router |
|---|---|
| `pages/foo.tsx` | `app/foo/page.tsx` |
| `_app.tsx` | Root/nested layouts y providers |
| `_document.tsx` | Root layout |
| `next/head` | Metadata API |
| `getStaticProps` | Server Component + cache policy |
| `getServerSideProps` | Server Component request-time |
| `getStaticPaths` | `generateStaticParams` |
| API Routes | Route Handlers cuando conviene |
| `next/router` | `next/navigation` |
| Page loading manual | `loading.tsx` / Suspense |
| 500/404 pages | `error.tsx`, `not-found.tsx` |
Estas son equivalencias aproximadas, no reemplazos mecánicos uno a uno.
Elige una ruta:
Migra ruta, detalle, datos, metadata y acciones relacionadas. Así descubres incompatibilidades reales sin bloquear toda la aplicación.
En Pages Router, un getLayout puede envolver páginas. En App Router:
app/
├─ layout.tsx
└─ dashboard/
├─ layout.tsx
└─ orders/page.tsxLos layouts persisten durante navegación y no reciben pathname como prop. No intentes convertir _app completo en un root layout gigante.
Separa providers cliente:
"use client";
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}Renderízalos lo más abajo posible para limitar JavaScript.
Antes:
export async function getServerSideProps(context) {
const order = await getOrder(context.params.id);
return { props: { order } };
}Después:
export default async function OrderPage({
params,
}: PageProps<"/orders/[id]">) {
const { id } = await params;
const order = await getAccessibleOrder(id);
if (!order) notFound();
return <OrderDetails order={order} />;
}La lectura queda cerca de la UI y puede mantenerse server-only. Sigue necesitando autenticación, autorización, timeout y DTO.
Decide si el resultado:
use cache.No traduzcas revalidate: 3600 sin revisar freshness y Cache Components.
En Next.js 16 son valores asíncronos en las APIs actuales:
const { slug } = await params;
const filters = await searchParams;Usa codemods, pero revisa tipos y lógica. No esperes Promises request-time dentro de scopes cacheados durante prerender.
Una page de Pages Router es un componente cliente en la práctica. Al moverla a app, el default es Server Component.
No añadas "use client" a toda la page para evitar errores. Separa:
Server page
├─ data and structure
├─ static components
└─ client islands
├─ filters
├─ modal
└─ interactive table controlsLos props que cruzan deben ser serializables y mínimos.
Antes:
import { useRouter } from "next/router";Después:
import {
usePathname,
useRouter,
useSearchParams,
} from "next/navigation";Diferencias:
router.query igual.router.refresh() solicita un nuevo RSC payload.Compatibilidad durante coexistencia puede usar next/compat/router en componentes compartidos específicos, pero elimina esa capa cuando termine la migración.
Antes:
<Head>
<title>{project.title}</title>
</Head>Después:
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params;
const project = await getProject(slug);
return { title: project.title };
}Comparte loader con la page mediante React.cache o una DAL. Migra canonical, OG, robots, icons y JSON-LD; no solo title.
Pages Router puede mostrar un spinner cliente tras navegación. App Router permite:
loading.tsx por segmento.Diseña una shell estable y boundaries por unidad visual. No agregues una boundary por cada componente.
Prueba hard refresh: la navegación prefetcheada puede esconder fallos que aparecen al cargar directamente.
Migra:
notFound → notFound() y not-found.tsx.error.tsx.global-error.tsx cuando aplica.No captures redirect() o notFound() con catches genéricos.
Puedes conservar endpoints HTTP mientras migras. Server Actions aportan integración con forms internos:
form
→ Action
→ validate
→ authenticate/authorize
→ transaction
→ invalidate
→ redirect/resultNo conviertas todas las API Routes en Actions. Webhooks y consumidores externos siguen necesitando HTTP.
Migra cuando:
app.No migres si el endpoint estable funciona y el cambio no aporta valor inmediato. Un Handler y una page no pueden ocupar la misma URL final.
No limites la migración a mover redirects. Verifica:
Una página protegida visualmente puede seguir exponiendo una Action sin auth.
Es una de las diferencias más riesgosas. Documenta por loader:
scope
key
lifetime
invalidación
shared users
failure policyNo asumas que fetch se comporta igual que en tutoriales de Next.js 13/14. Si activas Cache Components, usa el modelo actual y evita mezclar route configs antiguos sin necesidad.
next/image y next/font pueden cambiar APIs según versión.Comprueba orden de CSS en producción. Dos routers coexistiendo pueden cargar estilos globales con efectos inesperados.
En Pages Router quizá escuchabas router.events. En App Router observa navegación mediante hooks o usa la integración de la plataforma.
Evita duplicar page views durante coexistencia. Distingue prefetch de navegación real.
Por vertical:
Ejecuta tests contra production build y preview.
Mientras ambos routers viven:
La duplicación temporal es aceptable si tiene fecha de eliminación.
build new route behind flag/rewrite
→ internal users
→ percentage rollout
→ compare metrics
→ switch canonical route
→ remove old codeNo mantengas dos implementaciones escribiendo al mismo dominio sin idempotencia y contratos consistentes.
Conserva el modelo antiguo y pierde beneficios.
Demasiado riesgo y feedback tardío.
Freshness cambia sin darse cuenta.
Actions/API quedan en un estado inseguro o duplicado.
Oculta problemas de acceso directo.
Duplica conocimiento y bugs.
app usa Server Components por defecto.use client a toda página migrada?Actualizaciones de Next.js y compatibilidad establece un proceso para adoptar versiones sin convertir toda la Wiki en información efímera.