App Router vs Pages Router en Next.js: Cual Usar y por qué
Comparación detallada entre App Router y Pages Router en Next.js. Routing, data fetching, layouts, SEO, rendimiento y migración con ejemplos de código.
App Router vs Pages Router en Next.js: Cual Usar
La decision entre App Router vs Pages Router en NextJS es probablemente la primera que tomas al crear un proyecto nuevo, y la más consecuente para toda la arquitectura de tu aplicación. No es solo una preferencia de sintaxis -- cada router implica un modelo de datos, rendering y composición fundamentalmente distinto.
Next.js mantiene ambos routers funcionales, pero la dirección del framework es clara: App Router es el presente y el futuro. Este artículo compara ambos con código real para que tomes una decision informada.
Pages Router: cómo funciona
Pages Router fue el sistema de routing original de NextJS. Lleva activo desde las primeras versiones del framework y durante años fue la única opción. Su modelo es simple y directo.
Estructura de archivos
Cada archivo dentro de la carpeta pages/ se convierte automáticamente en una ruta:
pages/
index.tsx --> /
about.tsx --> /about
contact.tsx --> /contact
blog/
index.tsx --> /blog
[slug].tsx --> /blog/cualquier-slug
api/
users.ts --> /api/users
posts/
[id].ts --> /api/posts/123
_app.tsx --> Layout global
_document.tsx --> HTML base
404.tsx --> página de error 404
El mapeo es 1:1 entre archivo y ruta. pages/about.tsx siempre será /about. No hay ambiguedad.
Data fetching en Pages Router
Pages Router usa funciones especiales que se exportan junto al componente de la página. Cada una tiene un propósito diferente:
getServerSideProps (SSR)
Se ejecuta en el servidor en cada request. útil cuando los datos cambian frecuentemente:
// pages/dashboard.tsx
import type { GetServerSideProps, InferGetServerSidePropsType } from 'next'
interface DashboardProps {
usuarios: Array<{ id: string; nombre: string; email: string }>
totalVentas: number
}
export const getServerSideProps: GetServerSideProps<DashboardProps> = async (context) => {
const { req, res, query, params } = context
// Acceso a cookies, headers, query params
const token = req.cookies.session_token
if (!token) {
return {
redirect: {
destination: '/login',
permanent: false,
},
}
}
const [usuariosRes, ventasRes] = await Promise.all([
fetch('https://api.ejemplo.com/usuarios', {
headers: { Authorization: `Bearer ${token}` },
}),
fetch('https://api.ejemplo.com/ventas/total', {
headers: { Authorization: `Bearer ${token}` },
}),
])
const usuarios = await usuariosRes.json()
const { total } = await ventasRes.json()
return {
props: {
usuarios,
totalVentas: total,
},
}
}
export default function Dashboard({
usuarios,
totalVentas,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
return (
<main>
<h1>Dashboard</h1>
<p>Total ventas: ${totalVentas}</p>
<ul>
{usuarios.map((u) => (
<li key={u.id}>
{u.nombre} -- {u.email}
</li>
))}
</ul>
</main>
)
}La función getServerSideProps recibe un context con la request, response, parámetros y query. Retorna un objeto con props que llegan directamente al componente.
getStaticProps (SSG)
Se ejecuta en build time. Genera HTML estático que se sirve desde un CDN:
// pages/blog/[slug].tsx
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
interface Post {
slug: string
título: string
contenido: string
fecha: string
}
export const getStaticPaths: GetStaticPaths = async () => {
const res = await fetch('https://api.ejemplo.com/posts')
const posts: Post[] = await res.json()
const paths = posts.map((post) => ({
params: { slug: post.slug },
}))
return {
paths,
fallback: 'blocking', // Genera páginas nuevas bajo demanda
}
}
export const getStaticProps: GetStaticProps<{ post: Post }> = async ({ params }) => {
const res = await fetch(`https://api.ejemplo.com/posts/${params?.slug}`)
if (!res.ok) {
return { notFound: true }
}
const post: Post = await res.json()
return {
props: { post },
revalidate: 3600, // ISR: regenera cada hora
}
}
export default function BlogPost({
post,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<article>
<h1>{post.título}</h1>
<time>{post.fecha}</time>
<div dangerouslySetInnerHTML={{ __html: post.contenido }} />
</article>
)
}Con getStaticPaths le dices a NextJS que rutas generar en build time. getStaticProps obtiene los datos para cada una. La opción revalidate habilita ISR (Incremental Static Regeneration) para actualizar el contenido sin hacer rebuild completo.
getStaticProps vs getServerSideProps
| Aspecto | getStaticProps | getServerSideProps |
|---|---|---|
| Cuando se ejecuta | Build time (+ ISR) | Cada request |
| Rendimiento | rápido (cache CDN) | Mas lento (servidor) |
| Datos | Pueden estar desactualizados | Siempre frescos |
| Uso ideal | Blog, docs, marketing | Dashboard, perfil, checkout |
| Fallback | blocking, true, false | No aplica |
| ISR | Si (revalidate) | No aplica |
Layout global en Pages Router
Pages Router tiene un sistema de layouts limitado. Usas _app.tsx para un layout global y componentes wrapper manuales para layouts por sección:
// pages/_app.tsx
import type { AppProps } from 'next/app'
import { Layout } from '@/components/Layout'
export default function App({ Component, pageProps }: AppProps) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}// components/Layout.tsx
import { Header } from './Header'
import { Footer } from './Footer'
export function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<Header />
<main>{children}</main>
<Footer />
</>
)
}El problema es que _app.tsx envuelve todas las páginas. Si necesitas un layout diferente para /dashboard y /blog, tienes que hacerlo manualmente con lógica condicional o con un patron getLayout:
// pages/_app.tsx
import type { AppProps } from 'next/app'
import type { NextPage } from 'next'
import type { ReactElement, ReactNode } from 'react'
type NextPageWithLayout = NextPage & {
getLayout?: (page: ReactElement) => ReactNode
}
type AppPropsWithLayout = AppProps & {
Component: NextPageWithLayout
}
export default function App({ Component, pageProps }: AppPropsWithLayout) {
const getLayout = Component.getLayout ?? ((page) => page)
return getLayout(<Component {...pageProps} />)
}// pages/dashboard.tsx
import { DashboardLayout } from '@/components/DashboardLayout'
import type { ReactElement } from 'react'
export default function DashboardPage() {
return <div>Contenido del dashboard</div>
}
DashboardPage.getLayout = function getLayout(page: ReactElement) {
return <DashboardLayout>{page}</DashboardLayout>
}Funciona, pero es un workaround. Los layouts no se preservan entre navegaciones y se re-renderizan completamente cada vez que cambias de página.
App Router: cómo funciona
App Router fue introducido en NextJS 13 y se volvio estable en NextJS 13.4. Usa la carpeta app/ en lugar de pages/ y trae un modelo completamente nuevo basado en Server Components.
Estructura de archivos
App Router usa convenciones de archivos especiales dentro de cada carpeta de ruta:
app/
layout.tsx --> Layout raiz (envuelve todo)
page.tsx --> /
loading.tsx --> UI de carga para /
error.tsx --> UI de error para /
not-found.tsx --> página 404 personalizada
about/
page.tsx --> /about
blog/
layout.tsx --> Layout para /blog y subrutas
page.tsx --> /blog
[slug]/
page.tsx --> /blog/cualquier-slug
loading.tsx --> UI de carga para posts
dashboard/
layout.tsx --> Layout del dashboard (persistente)
page.tsx --> /dashboard
settings/
page.tsx --> /dashboard/settings
analytics/
page.tsx --> /dashboard/analytics
api/
users/
route.ts --> /api/users (GET, POST, etc.)
posts/
[id]/
route.ts --> /api/posts/123
La diferencia fundamental: en App Router, cada carpeta puede tener archivos con propositos especificos (page.tsx, layout.tsx, loading.tsx, error.tsx, template.tsx). No es solo una carpeta = una ruta, sino una carpeta = un segmento de ruta con toda su configuración.
Archivos especiales de App Router
| Archivo | propósito |
|---|---|
page.tsx | UI de la ruta. Hace que la carpeta sea accesible como URL |
layout.tsx | Layout compartido. Se preserva entre navegaciones |
loading.tsx | UI de carga automática (usa Suspense internamente) |
error.tsx | Boundary de error (usa Error Boundary internamente) |
template.tsx | Similar a layout pero se re-monta en cada navegación |
not-found.tsx | UI para cuando la página no existe |
route.ts | API Route handler (reemplaza pages/api) |
Data fetching en App Router
App Router elimina getServerSideProps, getStaticProps y getStaticPaths. En su lugar, haces fetch directamente en los Server Components:
Equivalente a getServerSideProps
// app/dashboard/page.tsx
// Server Component por defecto, se ejecuta en cada request
interface Usuario {
id: string
nombre: string
email: string
}
async function obtenerUsuarios(token: string): Promise<Usuario[]> {
const res = await fetch('https://api.ejemplo.com/usuarios', {
headers: { Authorization: `Bearer ${token}` },
cache: 'no-store', // Equivalente a getServerSideProps: datos frescos siempre
})
if (!res.ok) throw new Error('Error al obtener usuarios')
return res.json()
}
export default async function DashboardPage() {
const { cookies } = await import('next/headers')
const cookieStore = await cookies()
const token = cookieStore.get('session_token')?.value
if (!token) {
const { redirect } = await import('next/navigation')
redirect('/login')
}
const usuarios = await obtenerUsuarios(token)
return (
<main>
<h1>Dashboard</h1>
<ul>
{usuarios.map((u) => (
<li key={u.id}>
{u.nombre} -- {u.email}
</li>
))}
</ul>
</main>
)
}La clave es cache: 'no-store' en el fetch. Esto le dice a NextJS que no cachee la respuesta y la obtenga fresca en cada request, similar a cómo funciona getServerSideProps.
Equivalente a getStaticProps + ISR
// app/blog/[slug]/page.tsx
interface Post {
slug: string
título: string
contenido: string
fecha: string
}
async function obtenerPost(slug: string): Promise<Post | null> {
const res = await fetch(`https://api.ejemplo.com/posts/${slug}`, {
next: { revalidate: 3600 }, // ISR: revalida cada hora
})
if (!res.ok) return null
return res.json()
}
// Equivalente a getStaticPaths
export async function generateStaticParams() {
const res = await fetch('https://api.ejemplo.com/posts')
const posts: Post[] = await res.json()
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await obtenerPost(slug)
if (!post) {
const { notFound } = await import('next/navigation')
notFound()
}
return (
<article>
<h1>{post.título}</h1>
<time>{post.fecha}</time>
<div dangerouslySetInnerHTML={{ __html: post.contenido }} />
</article>
)
}generateStaticParams reemplaza a getStaticPaths. La opción next: { revalidate: 3600 } en fetch reemplaza a revalidate de getStaticProps.
Layouts en App Router
Los layouts son una de las mejores mejoras de App Router. Son persistentes: no se re-renderizan cuando navegas entre rutas del mismo segmento.
// app/layout.tsx (Layout raiz - aplica a TODAS las páginas)
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Mi Sitio',
default: 'Mi Sitio',
},
description: 'Descripción del sitio',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<body>
<header>
<nav>
<a href="/">Inicio</a>
<a href="/blog">Blog</a>
<a href="/dashboard">Dashboard</a>
</nav>
</header>
<main>{children}</main>
<footer>
<p>Footer del sitio</p>
</footer>
</body>
</html>
)
}// app/dashboard/layout.tsx (Layout solo para /dashboard/*)
import { DashboardNav } from '@/components/DashboardNav'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<DashboardNav />
<div className="flex-1 p-6">{children}</div>
</div>
)
}Cuando navegas de /dashboard a /dashboard/settings, el DashboardLayout no se re-renderiza. Solo cambia el children. Esto es imposible de lograr en Pages Router sin workarounds.
Metadata y SEO en App Router
App Router tiene un sistema nativo de metadata que genera automáticamente las tags de SEO:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
// Metadata dinámica basada en los parámetros
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params
const post = await obtenerPost(slug)
if (!post) {
return { title: 'Post no encontrado' }
}
return {
title: post.título,
description: post.resumen,
openGraph: {
title: post.título,
description: post.resumen,
type: 'article',
publishedTime: post.fecha,
images: [{ url: post.imagen }],
},
twitter: {
card: 'summary_large_image',
title: post.título,
description: post.resumen,
},
}
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await obtenerPost(slug)
// ...
}Para una estrategia de SEO completa, un sitemap automático complementa la metadata individual de cada página.
En Pages Router, la metadata requiere el componente Head de next/head en cada página:
// pages/blog/[slug].tsx (Pages Router)
import Head from 'next/head'
export default function BlogPost({ post }) {
return (
<>
<Head>
<title>{post.título}</title>
<meta name="description" content={post.resumen} />
<meta property="og:title" content={post.título} />
<meta property="og:description" content={post.resumen} />
</Head>
<article>
<h1>{post.título}</h1>
</article>
</>
)
}La diferencia: con App Router, la metadata se maneja de forma declarativa y tipada. Con Pages Router, manejas HTML tags manualmente.
Tabla comparativa detallada
| Aspecto | Pages Router | App Router |
|---|---|---|
| Carpeta | pages/ | app/ |
| Modelo de rendering | Todo es Client Component | Server Components por defecto |
| Data fetching | getServerSideProps, getStaticProps | async components + fetch |
| Rutas estáticas | getStaticPaths | generateStaticParams |
| Layouts | _app.tsx + getLayout manual | layout.tsx anidados y persistentes |
| Loading UI | Manual con estados | loading.tsx automático |
| Error handling | _error.tsx global | error.tsx por segmento |
| Metadata/SEO | next/head manual | generateMetadata tipado |
| API Routes | pages/api/*.ts | app/api/*/route.ts |
| Router hook | useRouter de next/router | useRouter de next/navigation |
| Streaming SSR | No nativo | Si, con Suspense |
| Server Actions | No disponible | Si ("use server") |
| Parallel Routes | No disponible | Si (@folder) |
| Intercepting Routes | No disponible | Si ((..)folder) |
| Route Groups | No disponible | Si ((folder)) |
| Middleware | Si | Si (mismo sistema) |
| Sitemap nativo | No | Si (sitemap.ts) |
| Bundle size | Todo va al cliente | Solo Client Components |
| Status | Mantenimiento (sin features nuevas) | Desarrollo activo |
Routing avanzado exclusivo de App Router
App Router introduce conceptos de routing que no tienen equivalente en Pages Router.
Route Groups
Permiten organizar rutas sin afectar la URL:
app/
(marketing)/
page.tsx --> /
about/
page.tsx --> /about
pricing/
page.tsx --> /pricing
layout.tsx --> Layout para marketing
(dashboard)/
dashboard/
page.tsx --> /dashboard
settings/
page.tsx --> /dashboard/settings
layout.tsx --> Layout para dashboard
Los parentesis en (marketing) y (dashboard) no aparecen en la URL. Sirven solo para agrupar rutas que comparten el mismo layout.
Parallel Routes
Permiten renderizar múltiples páginas en la misma vista simultaneamente:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
equipo,
}: {
children: React.ReactNode
analytics: React.ReactNode
equipo: React.ReactNode
}) {
return (
<div>
{children}
<div className="grid grid-cols-2 gap-4">
{analytics}
{equipo}
</div>
</div>
)
}app/
dashboard/
page.tsx
@analytics/
page.tsx
loading.tsx
@equipo/
page.tsx
loading.tsx
layout.tsx
Cada slot (@analytics, @equipo) se carga de forma independiente. Si analytics tarda 3 segundos y equipo tarda 1, el usuario ve equipo mientras analytics muestra su loading.tsx.
Intercepting Routes
Permiten abrir una ruta en un modal sin navegar completamente:
app/
feed/
page.tsx --> /feed (lista de fotos)
(..)foto/[id]/
page.tsx --> Intercepta /foto/123 y muestra en modal
foto/
[id]/
page.tsx --> /foto/123 (página completa, acceso directo)Cuando haces click en una foto desde el feed, se abre en un modal (ruta interceptada). Si compartes el link /foto/123 directamente, se abre como página completa. Instagram funciona exactamente así.
Migración de Pages Router a App Router
Si tienes un proyecto existente con Pages Router, la migración es gradual. No necesitas convertir todo de una vez.
Estrategia recomendada
Crea la carpeta app/ junto a pages/
Ambas pueden coexistir. Las rutas en app/ tienen prioridad si hay conflicto.
Mueve el layout global primero
Convierte _app.tsx y _document.tsx en app/layout.tsx:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Mi Sitio',
description: 'Descripción',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<body>{children}</body>
</html>
)
}Migra una página simple para probar
Elige una página sin data fetching complejo (como /about) y muevela:
// Antes: pages/about.tsx
export default function About() {
return <h1>Sobre nosotros</h1>
}
// después: app/about/page.tsx
export default function AboutPage() {
return <h1>Sobre nosotros</h1>
}Convierte el data fetching progresivamente
Reemplaza getServerSideProps con Server Components:
// Antes: pages/products.tsx
export async function getServerSideProps() {
const res = await fetch('https://api.ejemplo.com/products')
const products = await res.json()
return { props: { products } }
}
export default function Products({ products }) {
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}
// después: app/products/page.tsx
export default async function ProductsPage() {
const res = await fetch('https://api.ejemplo.com/products', {
cache: 'no-store',
})
const products = await res.json()
return (
<ul>
{products.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
)
}Actualiza las importaciones del router
// Antes (Pages Router)
import { useRouter } from 'next/router'
const router = useRouter()
router.push('/destino')
router.query.slug // parámetros de la URL
// después (App Router)
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
router.push('/destino')
// Los params llegan como prop en page.tsxElimina la carpeta pages/ cuando todo este migrado
Una vez que todas las rutas estan en app/, puedes eliminar pages/ y sus archivos especiales (_app.tsx, _document.tsx, _error.tsx).
Cambios especificos en la migración
Navegación
// Pages Router
import { useRouter } from 'next/router'
import Link from 'next/link'
function Componente() {
const router = useRouter()
// Ruta actual
const ruta = router.pathname // '/blog/[slug]'
const rutaReal = router.asPath // '/blog/mi-post'
// Query params
const { slug } = router.query // { slug: 'mi-post' }
// Navegar
router.push('/destino')
router.replace('/destino')
router.back()
return <Link href="/blog">Blog</Link>
}// App Router
'use client'
import { useRouter, usePathname, useSearchParams, useParams } from 'next/navigation'
import Link from 'next/link'
function Componente() {
const router = useRouter()
const pathname = usePathname() // '/blog/mi-post'
const searchParams = useSearchParams() // URLSearchParams
const params = useParams() // { slug: 'mi-post' }
// Navegar
router.push('/destino')
router.replace('/destino')
router.back()
router.refresh() // Nuevo: re-ejecuta Server Components
return <Link href="/blog">Blog</Link>
}La diferencia principal: en App Router, useRouter viene de next/navigation (no next/router), y las funcionalidades estan separadas en hooks individuales (usePathname, useSearchParams, useParams).
Importación correcta
Si importas useRouter de next/router dentro de app/, vas a obtener un error. Asegúrate de importar siempre de next/navigation en componentes dentro de la carpeta app/.
API Routes
// Pages Router: pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next'
export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'GET') {
res.status(200).json({ users: [] })
} else if (req.method === 'POST') {
const body = req.body
res.status(201).json({ created: true })
} else {
res.status(405).end()
}
}// App Router: app/api/users/route.ts
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({ users: [] })
}
export async function POST(request: Request) {
const body = await request.json()
return NextResponse.json({ created: true }, { status: 201 })
}En App Router, cada método HTTP es una función exportada separada. No necesitas el switch/if sobre req.method. además, usa las APIs estandar de Request y Response del Web Platform.
Error handling
// Pages Router: pages/_error.tsx (global, único)
function ErrorPage({ statusCode }) {
return <p>Error: {statusCode}</p>
}
ErrorPage.getInitialProps = ({ res, err }) => {
const statusCode = res ? res.statusCode : err ? err.statusCode : 404
return { statusCode }
}
export default ErrorPage// App Router: app/blog/error.tsx (por segmento, granular)
'use client' // error.tsx debe ser Client Component
export default function BlogError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>Error al cargar el blog</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>Intentar de nuevo</button>
</div>
)
}App Router permite error boundaries por segmento de ruta. Si falla la carga del blog, solo la sección del blog muestra el error -- el header, footer y sidebar siguen funcionando.
cuándo usar cada uno
Usa App Router cuando:
- Proyecto nuevo: No hay razon para empezar con Pages Router en 2026
- SEO es prioridad: Metadata tipada, sitemaps nativos, Server Components reducen JS
- Layouts complejos: Dashboards, paneles admin, sitios con sidebars persistentes
- Rendimiento importa: Server Components, streaming, bundle reducido
- Necesitas funcionalidades nuevas: Server Actions, Parallel Routes, Intercepting Routes
Usa Pages Router cuando:
- Proyecto existente estable: Si funciona y no necesita features nuevas, no lo toques
- Equipo no tiene tiempo para migrar: La migración tiene costo y riesgo
- Dependencias incompatibles: Algunas librerías antiguas no funcionan con Server Components
- Prototipo rápido: Si la velocidad de desarrollo importa más que la arquitectura (aunque hoy App Router ya es igual de rápido para prototipar)
Recomendación directa
Si vas a crear un proyecto nuevo, usa App Router. El ecosistema, las herramientas, la documentación, y las mejoras de rendimiento estan todas enfocadas ahi. Pages Router es para mantener lo que ya existe.
Rendimiento comparado
Bundle size
Con Pages Router, todo el código del componente de la página va al navegador, incluyendo lógica de renderizado, formateo de datos y dependencias.
Con App Router, solo los Client Components envian JavaScript al navegador. Un blog post que se renderiza con marked (500KB) en un Server Component envia 0KB de JavaScript adicional al cliente.
Time to First Byte (TTFB)
Ambos routers pueden lograr tiempos similares con SSR. La diferencia esta en el streaming: App Router puede empezar a enviar HTML antes de que todo este listo, mientras Pages Router espera a que getServerSideProps termine completamente.
// App Router con streaming
// El usuario ve el header inmediatamente mientras las secciones cargan
import { Suspense } from 'react'
export default function DashboardPage() {
return (
<div>
<h1>Dashboard</h1>
<Suspense fallback={<Skeleton />}>
<GraficaVentas />
</Suspense>
<Suspense fallback={<Skeleton />}>
<TablaUsuarios />
</Suspense>
</div>
)
}Con Pages Router, el usuario no ve nada hasta que todas las queries del getServerSideProps terminan. Con App Router y Suspense, el usuario ve contenido progresivamente.
Navegación entre páginas
En Pages Router, cambiar de página descarga el JavaScript de la nueva página, ejecuta getServerSideProps (si aplica), y renderiza el componente. El layout se destruye y se recrea.
En App Router, los layouts se preservan. Solo se actualiza el segmento que cambia. Esto significa menos JavaScript descargado, menos trabajo del navegador, y transiciones más fluidas.
Estructura recomendada para App Router
Para un proyecto nuevo con App Router, esta estructura cubre la mayoria de los casos:
app/
(marketing)/
layout.tsx
page.tsx --> /
about/
page.tsx --> /about
blog/
page.tsx --> /blog
[slug]/
page.tsx --> /blog/post-slug
(app)/
layout.tsx
dashboard/
page.tsx --> /dashboard
loading.tsx
error.tsx
settings/
page.tsx --> /dashboard/settings
analytics/
page.tsx --> /dashboard/analytics
api/
users/
route.ts
webhooks/
route.ts
layout.tsx --> Root layout
not-found.tsx
sitemap.ts
robots.ts
Los Route Groups (marketing) y (app) separan las secciones publicas del area autenticada, cada una con su propio layout, sin afectar las URLs.
Preguntas frecuentes
¿getInitialProps sigue funcionando en App Router?
No. getInitialProps solo funciona en Pages Router y se considera deprecada incluso ahi. En App Router, usa Server Components con async/await para obtener datos.
¿Puedo usar middleware con ambos routers?
Si. El middleware (middleware.ts en la raiz del proyecto) funciona igual con ambos routers. Se ejecuta antes del rendering, independientemente de si la ruta esta en pages/ o app/.
¿El rendimiento de Pages Router es peor?
No necesariamente. Pages Router con getStaticProps y un CDN puede ser muy rápido. La ventaja de App Router esta en Server Components (menos JS al cliente), streaming (contenido progresivo), y layouts persistentes (menos re-renders en navegación). Para páginas completamente estáticas, la diferencia es mínima.
¿Qué pasa con next/image y next/link en la migración?
next/image funciona igual en ambos routers. next/link también, pero en App Router ya no necesitas la prop legacyBehavior y el componente <a> interno se agrega automáticamente. En App Router, <Link href="/blog">Blog</Link> es suficiente -- no necesitas un <a> hijo.
¿Puedo usar ISR (Incremental Static Regeneration) en App Router?
Si. ISR en App Router se configura con la opción revalidate en fetch o con export const revalidate = 3600 a nivel de segmento de ruta. también puedes usar revalidatePath() y revalidateTag() para invalidación bajo demanda, qué es más flexible que el ISR basado en tiempo de Pages Router.
Recursos adicionales
- NextJS App Router Documentation -- documentación oficial del App Router con ejemplos y API reference
- NextJS Migration Guide: App Router -- guía paso a paso del equipo de NextJS para migrar de Pages Router a App Router
- NextJS Pages Router Documentation -- referencia de Pages Router, todavia mantenida por el equipo de NextJS
Preguntas frecuentes
¿Debería usar App Router o Pages Router para un proyecto nuevo en Next.js?
Para proyectos nuevos, usa App Router. Es el estandar recomendado por el equipo de NextJS, soporta Server Components, layouts anidados, streaming SSR y tiene mejor rendimiento por defecto. Pages Router sigue funcionando pero no recibe nuevas funcionalidades.
¿Puedo usar App Router y Pages Router en el mismo proyecto?
Si. NextJS permite usar ambos routers simultaneamente. Las rutas en app/ usan App Router y las rutas en pages/ usan Pages Router. Esto facilita la migración gradual, pero debes tener cuidado de no crear la misma ruta en ambos directorios porque causara un conflicto.
¿Qué reemplaza a getServerSideProps en App Router?
En App Router no necesitas funciones especiales para obtener datos. Los Server Components pueden hacer fetch directamente con async/await dentro del componente. Para revalidación, usas la opción next: { revalidate: seconds } en fetch o la función revalidatePath/revalidateTag para invalidación bajo demanda.
¿Es difícil migrar de Pages Router a App Router?
Depende del tamaño del proyecto. NextJS permite migración gradual -- puedes mover una ruta a la vez. Los cambios principales son: pasar de pages/ a app/, reemplazar getServerSideProps/getStaticProps con Server Components, adoptar el nuevo sistema de layouts, y cambiar de next/router a next/navigation.
¿Pages Router va a desaparecer de NextJS?
No en el corto plazo. El equipo de NextJS ha confirmado que Pages Router seguira siendo soportado por varias versiones más. Sin embargo, todas las nuevas funcionalidades se desarrollan exclusivamente para App Router, por lo que Pages Router no recibira mejoras nuevas.
Articulos relacionados
Bun vs Node.js en 2026: Benchmarks Reales
Comparativa práctica entre Bun y Node.js en 2026. Benchmarks reales, compatibilidad con Next.js, y cuando tiene sentido migrar.
Prisma vs Drizzle ORM en 2026: Cual Elegir para tu Proyecto
Comparación detallada entre Prisma y Drizzle ORM. Rendimiento, DX, migraciones, tipos, queries y cuándo usar cada uno en tu proyecto Next.js con TypeScript.