Saltar al contenido

De Pages Router a App Router sin congelar nada

Los dos routers conviven en la misma aplicación, así que la migración va ruta por ruta mientras tu equipo sigue publicando. Este es el orden que funciona y los tres puntos donde suele torcerse.

8 min de lectura

El dato más útil sobre esta migración es que no tienes que elegir. pages/ y app/ corren en la misma aplicación, en el mismo proceso, contra el mismo despliegue. Una petición se resuelve primero contra app/ y cae a pages/ si no encuentra nada.

Eso significa que congelar las features no es un requisito de la migración. Es un síntoma de haberla planificado como un cambio grande en vez de como cuarenta pequeños.

El orden que funciona

Migra desde las hojas hacia dentro, no desde la raíz hacia fuera.

  1. Primero una ruta de poco tráfico y poco riesgo. Algo como /about. No porque importe, sino porque te obliga a construir el layout, el helper de metadatos y las convenciones de datos que usarás en todo lo demás, en una página que nadie notará si se rompe una hora.
  2. El resto de rutas estáticas de marketing. Se convierten casi mecánicamente y son donde la ganancia de rendimiento es mayor.
  3. Rutas dinámicas de solo lectura. Fichas de producto, artículos, listados. Aquí conviertes getStaticProps y getServerSideProps en componentes async, que es el grueso del trabajo mecánico.
  4. Rutas autenticadas e interactivas. Paneles, ajustes, checkout. Llevan el riesgo real porque llevan las mutaciones.
  5. El layout raíz, al final. Mover _app.tsx y _document.tsx pronto mete todas las rutas en el árbol nuevo antes de que ninguna esté lista.

Los equipos que invierten esto -empezando por el armazón porque parece el cimiento- son los que acaban congelando.

En qué se convierte cada API de Pages

Pages RouterApp Router
getStaticPropscomponente async + fetch con revalidate o etiqueta
getServerSidePropscomponente async + cache: 'no-store'
getStaticPathsgenerateStaticParams
_app.tsxapp/layout.tsx
_document.tsxapp/layout.tsx (las etiquetas html y body)
next/headel export metadata o generateMetadata
useRouter().querylas props params y searchParams
Rutas de APIRoute Handlers, o Server Actions para mutaciones

La ruta suele quedar más corta. Esto:

// pages/products/[slug].tsx
export async function getStaticProps({ params }) {
  const product = await getProduct(params.slug);
  if (!product) return { notFound: true };
  return { props: { product }, revalidate: 3600 };
}
 
export async function getStaticPaths() {
  const products = await getProducts();
  return {
    paths: products.map((p) => ({ params: { slug: p.slug } })),
    fallback: 'blocking',
  };
}
 
export default function ProductPage({ product }) {
  return <Product data={product} />;
}

se convierte en esto:

// app/products/[slug]/page.tsx
export async function generateStaticParams() {
  const products = await getProducts();
  return products.map((p) => ({ slug: p.slug }));
}
 
export default async function ProductPage({ params }) {
  const { slug } = await params;
  const product = await getProduct(slug);
  if (!product) notFound();
 
  return <Product data={product} />;
}

Fíjate en await params. En las versiones actuales de Next.js, params y searchParams son promesas. El código copiado de un tutorial de hace dos años las desestructura directamente y falla de una forma que parece un problema de datos.

Qué comparten los dos routers y qué no

La coexistencia que hace posible una migración incremental no es total, y donde no lo es es donde una migración se atasca.

Comparten la petición. Un despliegue, un dominio, un next.config, y una ruta resuelve al router que la posee, con app/ ganando cuando ambos definen la misma ruta. Ese último detalle es el mecanismo sobre el que descansa toda la estrategia: añades la versión en App Router, gana, y el archivo antiguo se queda como un rollback que está a un borrado de la limpieza y a un revert de volver a estar vivo.

No comparten layout. Un pages/_app.tsx envuelve solo rutas de Pages y un app/layout.tsx solo rutas de App, así que todo lo global - analítica, un proveedor de tema, una cabecera - existe por duplicado mientras dure la migración. Duplícalo a propósito y borra una copia al final. Intentar evitar la duplicación importando uno dentro del otro es como un proveedor de cliente acaba envolviendo el árbol de servidor.

No comparten navegación en cliente. Un enlace de una ruta de Pages a una de App es una carga completa de documento, no una transición de cliente. Durante una migración larga eso aparece como una página que se siente más lenta que sus dos extremos, y se resuelve sola cuando el par cae del mismo lado.

Los tres puntos donde se tuerce

Marcar el armazón como use client para que la migración compile. Algo del layout viejo usa un proveedor de contexto, el build se queja, y alguien pone 'use client' en la primera línea del layout raíz. La aplicación compila, cada ruta manda el árbol entero al navegador, y el beneficio principal de la migración desaparece mientras el coste se sigue pagando entero. Baja los proveedores a un componente de cliente que envuelva children y deja el layout en el servidor. Es la misma disciplina de frontera que dónde te cuesta de verdad use client.

Portar la capa de datos sin tocarla. getServerSideProps corría una vez por ruta, así que casi todas las bases de código construyeron una función que lo pide todo por página. En el App Router cada componente puede pedir lo suyo, y las peticiones idénticas dentro de un render se deduplican solas. Mantener la función-dios funciona, y también mantiene la ruta entera esperando a la llamada más lenta: nada hace streaming y Suspense no te aporta nada.

Dejar next/head donde estaba. En app/ es silenciosamente inerte. Sin error, sin aviso: solo una ruta sin título y sin descripción, que nadie nota hasta que llega un informe de SEO un mes después. Búscalo con grep antes de publicar y convierte cada caso al export metadata.

Dejar la comprobación de autenticación para el final. La autenticación escrita para getServerSideProps devuelve una redirección desde una función que posee toda la ruta. En el App Router la decisión equivalente suele pertenecer a un layout o a la frontera de la propia ruta, y la forma es lo bastante distinta como para que los equipos lo aplacen, lo que significa que las rutas detrás son las últimas en moverse y la migración se para a un paso del final justo en las superficies que más importan. Mueve una ruta autenticada pronto, a propósito, para fijar el patrón mientras hay poco en juego.

Demostrar que funcionó, ruta a ruta

Antes de mover una ruta, registra lo que hace ahora: LCP e INP de campo, el HTML renderizado, el título y el canonical, el JSON-LD, el First Load JS. Después de moverla, compara. Una migración sin un "antes" es una reescritura con pasos de más.

Las rutas que hemos movido así suelen perder entre un 30 % y un 50 % de su JavaScript sin ganar riesgo, porque en ningún momento hubo más de una ruta en estado desconocido.

Cuándo parar

Una migración que llega al noventa por ciento y se queda ahí es un resultado común e infravalorado, y no es automáticamente un fracaso. Dos routers en una aplicación son una configuración soportada, no un estado roto, y el diez por ciento restante suele ser un puñado de rutas con una razón genuina para ser incómodas: una dependencia de un servidor propio, una librería sin historia de Server Components, una página que se va a sustituir el trimestre que viene de todos modos.

La prueba de si parar es aceptable es si las rutas de Pages que quedan te cuestan algo de forma continua. Si están aisladas, tienen poco tráfico y nadie las edita, terminar es pulcritud y no valor. Si tienen secuestrado el layout compartido - de modo que el lado App Router no puede quitar el proveedor duplicado -, entonces te están costando en todas las rutas, y ese es el argumento para terminar.

Lo que nunca es aceptable es parar sin dejarlo por escrito. Una migración sin un estado final documentado se convierte, en cuanto cambia una persona del equipo, en una base de código donde nadie sabe qué convención es la vigente y las dos se siguen copiando.

Y si la base de código que migras es además una que nadie quiere tocar, eso es otro problema encima de este: heredar una base de código Next.js que nadie quiere tocar cubre qué hacer primero.

Preguntas relacionadas

¿Pueden convivir los dos routers?
Sí, y de eso se trata. Coexisten en una misma aplicación, así que la migración va ruta a ruta y nada tiene que detenerse mientras ocurre.
¿Qué ruta movemos primero?
Una real pero no crítica. Una página de marketing le enseña el modelo al equipo. El checkout se lo enseña bajo presión.
¿Qué sustituye a getServerSideProps?
Nada, porque el propio componente pasa a ser el lugar donde se obtienen los datos. Ese es el cambio que más tarda en asentarse y el que merece hacerse bien en vez de mecánicamente.
¿Hay que moverlo todo?
Con el tiempo sí, si quiere un único modelo mental en el código. Pero una migración encallada que dejó atrás las rutas difíciles sigue siendo mejor que una reescritura que nunca llegó a salir.

← Volver a todos los artículos

Llamar+1 848 272 7583WhatsApp+90 850 308 5436Correoinfo@codefacture.comPágina de contacto