App Router vs Pages Router en Next.js: Cual Usar y Por Que
Comparacion detallada entre App Router y Pages Router en Next.js. Routing, data fetching, layouts, SEO, rendimiento y migracion con ejemplos de codigo.
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 mas consecuente para toda la arquitectura de tu aplicacion. No es solo una preferencia de sintaxis -- cada router implica un modelo de datos, rendering y composicion fundamentalmente distinto.
Next.js mantiene ambos routers funcionales, pero la direccion del framework es clara: App Router es el presente y el futuro. Este articulo compara ambos con codigo real para que tomes una decision informada.
Pages Router: como funciona
Pages Router fue el sistema de routing original de NextJS. Lleva activo desde las primeras versiones del framework y durante anos fue la unica opcion. Su modelo es simple y directo.
Estructura de archivos
Cada archivo dentro de la carpeta pages/ se convierte automaticamente 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 --> Pagina de error 404
El mapeo es 1:1 entre archivo y ruta. pages/about.tsx siempre sera /about. No hay ambiguedad.
Data fetching en Pages Router
Pages Router usa funciones especiales que se exportan junto al componente de la pagina. Cada una tiene un proposito diferente:
getServerSideProps (SSR)
Se ejecuta en el servidor en cada request. Util 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 funcion getServerSideProps recibe un context con la request, response, parametros y query. Retorna un objeto con props que llegan directamente al componente.
getStaticProps (SSG)
Se ejecuta en build time. Genera HTML estatico que se sirve desde un CDN:
// pages/blog/[slug].tsx
import type { GetStaticPaths, GetStaticProps, InferGetStaticPropsType } from 'next'
interface Post {
slug: string
titulo: 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 paginas 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.titulo}</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 opcion 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 | Rapido (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 seccion:
// 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 paginas. Si necesitas un layout diferente para /dashboard y /blog, tienes que hacerlo manualmente con logica 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 pagina.
App Router: como 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 --> Pagina 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 configuracion.
Archivos especiales de App Router
| Archivo | Proposito |
|---|---|
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 automatica (usa Suspense internamente) |
error.tsx | Boundary de error (usa Error Boundary internamente) |
template.tsx | Similar a layout pero se re-monta en cada navegacion |
not-found.tsx | UI para cuando la pagina 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 como funciona getServerSideProps.
Equivalente a getStaticProps + ISR
// app/blog/[slug]/page.tsx
interface Post {
slug: string
titulo: 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.titulo}</h1>
<time>{post.fecha}</time>
<div dangerouslySetInnerHTML={{ __html: post.contenido }} />
</article>
)
}generateStaticParams reemplaza a getStaticPaths. La opcion 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 paginas)
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: {
template: '%s | Mi Sitio',
default: 'Mi Sitio',
},
description: 'Descripcion 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 automaticamente las tags de SEO:
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
// Metadata dinamica basada en los parametros
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.titulo,
description: post.resumen,
openGraph: {
title: post.titulo,
description: post.resumen,
type: 'article',
publishedTime: post.fecha,
images: [{ url: post.imagen }],
},
twitter: {
card: 'summary_large_image',
title: post.titulo,
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 automatico complementa la metadata individual de cada pagina.
En Pages Router, la metadata requiere el componente Head de next/head en cada pagina:
// pages/blog/[slug].tsx (Pages Router)
import Head from 'next/head'
export default function BlogPost({ post }) {
return (
<>
<Head>
<title>{post.titulo}</title>
<meta name="description" content={post.resumen} />
<meta property="og:title" content={post.titulo} />
<meta property="og:description" content={post.resumen} />
</Head>
<article>
<h1>{post.titulo}</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 estaticas | getStaticPaths | generateStaticParams |
| Layouts | _app.tsx + getLayout manual | layout.tsx anidados y persistentes |
| Loading UI | Manual con estados | loading.tsx automatico |
| 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 multiples paginas 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 (pagina 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 pagina completa. Instagram funciona exactamente asi.
Migracion de Pages Router a App Router
Si tienes un proyecto existente con Pages Router, la migracion 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: 'Descripcion',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="es">
<body>{children}</body>
</html>
)
}Migra una pagina simple para probar
Elige una pagina sin data fetching complejo (como /about) y muevela:
// Antes: pages/about.tsx
export default function About() {
return <h1>Sobre nosotros</h1>
}
// Despues: 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>
)
}
// Despues: 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 // parametros de la URL
// Despues (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 migracion
Navegacion
// 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).
Importacion correcta
Si importas useRouter de next/router dentro de app/, vas a obtener un error. Asegurate 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 metodo HTTP es una funcion exportada separada. No necesitas el switch/if sobre req.method. Ademas, usa las APIs estandar de Request y Response del Web Platform.
Error handling
// Pages Router: pages/_error.tsx (global, unico)
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 seccion del blog muestra el error -- el header, footer y sidebar siguen funcionando.
Cuando 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 migracion tiene costo y riesgo
- Dependencias incompatibles: Algunas librerias antiguas no funcionan con Server Components
- Prototipo rapido: Si la velocidad de desarrollo importa mas que la arquitectura (aunque hoy App Router ya es igual de rapido para prototipar)
Recomendacion directa
Si vas a crear un proyecto nuevo, usa App Router. El ecosistema, las herramientas, la documentacion, 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 codigo del componente de la pagina va al navegador, incluyendo logica 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.
Navegacion entre paginas
En Pages Router, cambiar de pagina descarga el JavaScript de la nueva pagina, 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 mas 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 rapido. La ventaja de App Router esta en Server Components (menos JS al cliente), streaming (contenido progresivo), y layouts persistentes (menos re-renders en navegacion). Para paginas completamente estaticas, la diferencia es minima.
Que pasa con next/image y next/link en la migracion?
next/image funciona igual en ambos routers. next/link tambien, pero en App Router ya no necesitas la prop legacyBehavior y el componente <a> interno se agrega automaticamente. 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 opcion revalidate en fetch o con export const revalidate = 3600 a nivel de segmento de ruta. Tambien puedes usar revalidatePath() y revalidateTag() para invalidacion bajo demanda, que es mas flexible que el ISR basado en tiempo de Pages Router.
Recursos adicionales
- NextJS App Router Documentation -- documentacion oficial del App Router con ejemplos y API reference
- NextJS Migration Guide: App Router -- guia 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
Deberia 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 migracion gradual, pero debes tener cuidado de no crear la misma ruta en ambos directorios porque causara un conflicto.
Que 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 revalidacion, usas la opcion next: { revalidate: seconds } en fetch o la funcion revalidatePath/revalidateTag para invalidacion bajo demanda.
Es dificil migrar de Pages Router a App Router?
Depende del tamano del proyecto. NextJS permite migracion 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 mas. 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 practica 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
Comparacion detallada entre Prisma y Drizzle ORM. Rendimiento, DX, migraciones, tipos, queries y cuando usar cada uno en tu proyecto Next.js con TypeScript.