Next.js 13 introdujo el App Router (app/ directory), un cambio fundamental en cómo estructuramos aplicaciones Next.js. Pero el Pages Router (pages/ directory) sigue siendo totalmente válido y soportado. ¿Cuál usar? ¿Vale la pena migrar? Te lo explico todo.
Tabla Comparativa Rápida
| Característica | Pages Router (pages/) | App Router (app/) |
|---|---|---|
| Lanzamiento | Next.js 9 (2019) | Next.js 13 (2022) |
| Estado | Estable, mantenido | Estable desde Next.js 14 |
| Componentes | Solo Client Components | Server + Client Components |
| Layouts | _app.js + _document.js | Layouts anidados nativos |
| Data Fetching | getServerSideProps, getStaticProps | async components, fetch |
| Loading States | Manual | loading.js automático |
| Error Handling | _error.js global | error.js por ruta |
| Streaming | No | Sí (RSC + Suspense) |
Pages Router: El Clásico
Estructura de archivos
pages/
├── _app.js # Layout global
├── _document.js # HTML document
├── index.js # → /
├── about.js # → /about
├── blog/
│ ├── index.js # → /blog
│ └── [slug].js # → /blog/post-1
└── api/
└── hello.js # → /api/hello
Ejemplo: Página con Data Fetching
// pages/blog/[slug].js
export default function BlogPost({ post }) {
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
// Server-side rendering
export async function getServerSideProps({ params }) {
const post = await fetch(`https://api.com/posts/${params.slug}`).then((r) =>
r.json()
);
return { props: { post } };
}
// O Static Generation
export async function getStaticProps({ params }) {
const post = await fetch(`https://api.com/posts/${params.slug}`).then((r) =>
r.json()
);
return { props: { post } };
}
export async function getStaticPaths() {
const posts = await fetch("https://api.com/posts").then((r) => r.json());
return {
paths: posts.map((p) => ({ params: { slug: p.slug } })),
fallback: false,
};
}
Layout Global con _app.js
// pages/_app.js
import "../styles/globals.css";
import Navbar from "../components/Navbar";
import Footer from "../components/Footer";
export default function App({ Component, pageProps }) {
return (
<>
<Navbar />
<Component {...pageProps} />
<Footer />
</>
);
}
Problema: Solo hay un layout global. Si quieres layouts diferentes por sección (blog vs dashboard), necesitas lógica condicional.
App Router: La Nueva Era
Estructura de archivos
app/
├── layout.js # Root layout
├── page.js # → /
├── loading.js # Loading UI
├── error.js # Error handling
├── about/
│ └── page.js # → /about
├── blog/
│ ├── layout.js # Layout solo para /blog/*
│ ├── page.js # → /blog
│ └── [slug]/
│ ├── page.js # → /blog/post-1
│ └── loading.js
└── api/
└── hello/
└── route.js # → /api/hello
Server Components por Defecto
// app/blog/[slug]/page.js
// Este es un SERVER COMPONENT (corre en el servidor)
export default async function BlogPost({ params }) {
const { slug } = await params;
// Fetch directo en el componente, sin getServerSideProps
const post = await fetch(`https://api.com/posts/${slug}`, {
next: { revalidate: 60 }, // ISR automático
}).then((r) => r.json());
return (
<div>
<h1>{post.title}</h1>
<p>{post.content}</p>
</div>
);
}
// generateStaticParams reemplaza getStaticPaths
export async function generateStaticParams() {
const posts = await fetch("https://api.com/posts").then((r) => r.json());
return posts.map((p) => ({ slug: p.slug }));
}
Layouts Anidados
// app/layout.js (root)
export default function RootLayout({ children }) {
return (
<html lang="es">
<body>
<Header />
{children}
<Footer />
</body>
</html>
);
}
// app/blog/layout.js (solo para /blog/*)
export default function BlogLayout({ children }) {
return (
<div className="blog-container">
<aside>
<h3>Artículos Recientes</h3>
</aside>
<main>{children}</main>
</div>
);
}
Ventaja: Cada sección puede tener su propio layout, y se anidan automáticamente.
Loading States Automáticos
// app/blog/loading.js
export default function Loading() {
return (
<div className="spinner">
<p>Cargando artículos...</p>
</div>
);
}
Next.js muestra este componente automáticamente mientras page.js hace fetch de datos.
Error Handling por Ruta
// app/blog/error.js
"use client"; // Error boundaries deben ser Client Components
export default function Error({ error, reset }) {
return (
<div>
<h2>Algo salió mal!</h2>
<p>{error.message}</p>
<button onClick={reset}>Reintentar</button>
</div>
);
}
Client Components en App Router
Si necesitas interactividad (hooks, eventos, etc.), usa 'use client':
// app/components/Counter.js
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>Clicks: {count}</button>;
}
Data Fetching: Comparación
App Router
// SSR (por defecto)
async function getData() {
const res = await fetch("https://api.com/data", { cache: "no-store" });
return res.json();
}
// SSG
async function getData() {
const res = await fetch("https://api.com/data", { cache: "force-cache" });
return res.json();
}
// ISR (Incremental Static Regeneration)
async function getData() {
const res = await fetch("https://api.com/data", {
next: { revalidate: 60 }, // Revalida cada 60 segundos
});
return res.json();
}
Streaming y Suspense
App Router soporta React Server Components + Suspense, permitiendo streaming:
// app/dashboard/page.js
import { Suspense } from "react";
async function SlowComponent() {
const data = await fetch("https://slow-api.com/data");
return <div>{/* renderiza data */}</div>;
}
export default function Dashboard() {
return (
<div>
<h1>Dashboard</h1>
<FastContent />
<Suspense fallback={<Skeleton />}>
<SlowComponent />
</Suspense>
</div>
);
}
El HTML inicial se envía de inmediato, y los componentes lentos hacen "streaming" cuando están listos.
¿Cuál Usar?
Usa Pages Router si:
- Tienes un proyecto existente que funciona bien
- Tu equipo no está familiarizado con Server Components
- Usas librerías que no son compatibles con RSC aún
- Necesitas estabilidad probada en producción
Usa App Router si:
- Empiezas un proyecto nuevo
- Quieres mejor rendimiento (Server Components reducen JavaScript)
- Necesitas layouts complejos y anidados
- Quieres streaming y loading states automáticos
- Valoras DX moderna (fetch directo, metadata API, etc.)
Migración Gradual
Puedes usar ambos al mismo tiempo:
mi-app/
├── app/
│ └── dashboard/ # Nuevas rutas en App Router
│ └── page.js
└── pages/
├── index.js # Rutas existentes en Pages Router
└── about.js
Next.js prioriza app/ sobre pages/ si ambos definen la misma ruta.
Conclusión
- Pages Router: Maduro, estable, familiar. Perfecto para proyectos existentes.
- App Router: Moderno, eficiente, con mejor DX. Ideal para proyectos nuevos.
Ambos están soportados y no hay prisa por migrar. Pero si empiezas algo nuevo, App Router te dará ventajas reales en rendimiento y DX.