Guía de introducción a Next.js App Router: conceptos clave y uso básico

La primera vez que abrí la documentación oficial de Next.js, me quedé perdido. En la barra lateral, “Pages Router” y “App Router” aparecían uno al lado del otro, como diciendo “elige el que quieras”. Pero la pregunta era: ¿cuál? ¿En qué se diferencian? La documentación no lo dejaba claro y, cuanto más leía, más confuso todo.
Con el tiempo entendí que Next.js tiene dos sistemas de enrutamiento completamente distintos. El antiguo se llama Pages Router: estable y fiable, pero sin algunas funciones nuevas. El nuevo es App Router, lanzado desde la v13, estable en la v13.4 y ya la dirección recomendada oficialmente.
Puede que te preguntes: “¿Tengo que aprender App Router? ¿Será otra cosa más que complica la vida?”
Este artículo está pensado para resolver esa duda. Te explico los conceptos clave de App Router de la forma más simple posible: qué son Server Components, cómo usar los archivos especiales y en qué se diferencia de Pages Router. Al terminar, podrás empezar rápido y evitar errores típicos.
¿Qué es App Router? ¿Por qué usarlo?
En pocas palabras, App Router es el nuevo sistema de rutas que Next.js lanzó en la v13. Se apoya en las últimas características de React —Server Components (componentes de servidor)— y ofrece un diseño más moderno y flexible.
Frente al Pages Router antiguo, hay tres ventajas claras:
1. Mejor rendimiento
App Router usa componentes de servidor por defecto. Eso significa que gran parte del código corre en el servidor y el navegador descarga menos JavaScript, así que la página carga más rápido. Según el informe de Vercel de 2024, más del 60 % de las aplicaciones Next.js de primer nivel ya han migrado a App Router.
"Más del 60 % de las aplicaciones Next.js de primer nivel ya han migrado a App Router"
2. Sistema de layouts más flexible
En Pages Router, los layouts anidados son incómodos. En App Router, un archivo layout.js basta, y al cambiar de página el layout no se vuelve a renderizar: la experiencia es muy fluida.
3. Manejo de errores y estados de carga más potente
Puedes definir animaciones de carga con loading.js y capturar errores con error.js para mostrar una UI de respaldo. En Pages Router había que implementarlo a mano; App Router lo deja acordado por convención.
¿Pages Router sigue sirviendo? Sí.
Los dos sistemas pueden coexistir, pero si empiezas ahora con Next.js, te recomiendo ir directo a App Router. Es la dirección oficial y, desde la v14.1.4, el scaffolding de proyectos nuevos usa App Router por defecto.
Enrutamiento por sistema de archivos: de carpetas a páginas
El concepto central de App Router es: la estructura de carpetas es la estructura de rutas.
Suena abstracto, pero con un ejemplo se entiende:
app/
├── page.js # Inicio, corresponde a /
├── about/
│ └── page.js # Acerca de, corresponde a /about
└── blog/
├── page.js # Listado del blog, corresponde a /blog
└── [slug]/
└── page.js # Detalle del blog, corresponde a /blog/:slug
Puntos clave:
1. page.js es la entrada de la ruta
Solo los archivos llamados page.js se convierten en páginas accesibles. El resto (layout.js, loading.js, etc.) son archivos de soporte y no se visitan directamente.
2. Rutas dinámicas con corchetes
¿Quieres una ruta dinámica como /blog/hello-world? Crea app/blog/[slug]/page.js y el parámetro slug llegará al componente:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
return <h1>Artículo: {params.slug}</h1>
}
3. Captura de todas las rutas con [...slug]
A veces necesitas coincidir con rutas de varios niveles, por ejemplo /docs/a/b/c. Usa app/docs/[...slug]/page.js; params.slug será un array ['a', 'b', 'c'].
Comparación con Pages Router:
Si ya usaste Pages Router, verás que era pages/blog/[id].js. App Router usa app/blog/[id]/page.js, con una carpeta más. ¿Por qué? Para dejar espacio a layout.js, loading.js y otros archivos especiales en cada ruta.
Al principio puede parecer más trabajo, pero acostumbrándote la estructura del proyecto queda mucho más clara.
Server Components vs Client Components: el concepto central
Puede ser lo más confuso de App Router. A mí también me costó al principio.
En resumen: en App Router los componentes corren en el servidor por defecto; solo en el navegador cuando hace falta interactividad.
Por defecto es Server Component
Los componentes que creas en app/ son Server Components por defecto. Se renderizan en el servidor y el HTML llega directo al navegador.
Ventajas claras:
- Menos JavaScript: el código no se envía al navegador; el usuario descarga archivos JS más pequeños
- Acceso directo a recursos del backend: consultas a base de datos, claves de API y datos sensibles sin problema
- Carga inicial rápida: el servidor entrega HTML listo; el FCP (First Contentful Paint) es más corto
Ejemplo típico de Server Component:
// app/products/page.js
// Server Component: se ejecuta en el servidor
async function getProducts() {
const res = await fetch('https://api.example.com/products')
return res.json()
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Lista de productos</h1>
{products.map(p => (
<div key={p.id}>{p.name}</div>
))}
</div>
)
}
¿Ves? Puedes usar async/await para datos sin useEffect ni getServerSideProps.
¿Cuándo usar Client Component?
Hay escenarios que deben ejecutarse en el navegador:
- Usar hooks de React (
useState,useEffect) - Manejar interacción (
onClick,onChange) - Usar APIs del navegador (
localStorage,window)
Ahí entra Client Component. La marca es simple: una línea 'use client' al inicio del archivo:
// components/AddToCartButton.js
'use client' // Marca como Client Component
import { useState } from 'react'
export default function AddToCartButton({ productId }) {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
Añadir al carrito ({count})
</button>
)
}
Uso mixto: buenas prácticas
Lo potente es combinar ambos tipos.
En una página de productos:
- Lista de productos con Server Component (datos en servidor, menos JS)
- Botón de añadir al carrito con Client Component (clicks)
// app/products/page.js (Server Component)
import AddToCartButton from '@/components/AddToCartButton' // Client Component
async function getProducts() {
// Obtener datos en el servidor
}
export default async function ProductsPage() {
const products = await getProducts()
return (
<div>
<h1>Lista de productos</h1>
{products.map(p => (
<div key={p.id}>
{p.name}
<AddToCartButton productId={p.id} />
</div>
))}
</div>
)
}
Una regla: Server Component por defecto; 'use client' solo cuando de verdad necesites interactividad.
No marques todo con 'use client' desde el principio — ¿en qué mejorarías respecto a no usar App Router?
Archivos especiales: un proyecto más profesional
App Router define nombres especiales: layout.js, loading.js, error.js… Al principio pueden parecer molestos, pero en la práctica ayudan mucho.
layout.js: layout compartido
Es el archivo especial más usado. Define el layout de un segmento de ruta y envuelve todas las páginas del mismo nivel y las hijas.
Por ejemplo, navegación y pie en toda la app:
// app/layout.js (layout raíz)
export default function RootLayout({ children }) {
return (
<html lang="es">
<body>
<nav>Barra de navegación</nav>
<main>{children}</main>
<footer>Pie de página</footer>
</body>
</html>
)
}
También puedes anidar layouts:
app/
├── layout.js # Layout global (nav + pie)
├── page.js # Inicio
└── dashboard/
├── layout.js # Layout del panel (barra lateral)
├── page.js # /dashboard
└── settings/
└── page.js # /dashboard/settings
Al ir de /dashboard a /dashboard/settings, el layout global y el del panel no se vuelven a renderizar; solo cambia page.js. Muy fluido.
loading.js: estado de carga
Sin gestionar loading con useState. Crea loading.js y App Router envuelve la página con Suspense:
// app/dashboard/loading.js
export default function Loading() {
return <div>Cargando...</div>
}
Mientras se obtienen datos, se muestra el contenido de loading.js. Así de simple.
error.js: límite de errores
Captura errores de la página y muestra una UI de respaldo:
// app/dashboard/error.js
'use client' // Los error boundaries deben ser Client Component
export default function Error({ error, reset }) {
return (
<div>
<h2>Error: {error.message}</h2>
<button onClick={reset}>Reintentar</button>
</div>
)
}
Ojo con esto: error.js no captura errores del layout.js del mismo nivel. Es la limitación de React Error Boundary: solo errores de hijos, no del propio boundary ni de padres.
Para errores de layout.js, pon error.js en el directorio padre o usa global-error.js en la raíz.
not-found.js: página 404
Se muestra cuando la ruta no existe:
// app/not-found.js
export default function NotFound() {
return <h1>Página no encontrada</h1>
}
También puedes disparar un 404 desde el código:
import { notFound } from 'next/navigation'
export default async function BlogPost({ params }) {
const post = await getPost(params.slug)
if (!post) notFound() // Dispara not-found.js
return <article>{post.title}</article>
}
Relación entre archivos
Estos archivos tienen una jerarquía fija:
layout.js
├── loading.js (límite Suspense)
│ └── page.js
└── error.js (límite Error)
layout está más afuera; error.js no lo envuelve. loading.js cubre carga; error.js, errores.
Entender esta jerarquía evita sorpresas.
Obtención de datos: adiós a getServerSideProps
Si usaste Pages Router, seguro escribiste getServerSideProps o getStaticProps. Esas APIs son incómodas: exportar una función aparte y pasar datos no es muy directo.
App Router lo simplifica.
async/await directo
En Server Component puedes obtener datos dentro del componente:
// app/posts/page.js
async function getPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Es async/await normal, sin APIs especiales.
Obtención en paralelo
Puedes pedir varias fuentes a la vez:
export default async function Dashboard() {
// En paralelo, sin bloquearse
const [user, posts, stats] = await Promise.all([
getUser(),
getPosts(),
getStats()
])
return (
<div>
<h1>{user.name}</h1>
<Posts data={posts} />
<Stats data={stats} />
</div>
)
}
Caché y revalidación
Next.js cachea las peticiones fetch por defecto. Puedes controlar la estrategia:
// Revalidar cada 60 segundos
fetch('https://api.example.com/data', {
next: { revalidate: 60 }
})
// Sin caché: datos frescos siempre
fetch('https://api.example.com/data', {
cache: 'no-store'
})
Comparación con Pages Router:
- Pages Router:
getServerSideProps+getStaticProps, funciones exportadas aparte - App Router:
async/awaitdentro del componente
¿Más simple, no?
Preguntas frecuentes de principiantes y soluciones
Aprendiendo App Router tropecé con varios problemas. Aquí van los más habituales para que no repitas los mismos pasos.
Problema 1: ¿Cuándo usar ‘use client’?
La duda: en tutoriales aparece 'use client' por todas partes y no queda claro cuándo hace falta.
Solución:
Una regla: no lo añadas por defecto; solo cuando lo necesites.
Usa 'use client' solo si:
- Usas hooks de React (
useState,useEffect,useContext) - Hay interacción (
onClick,onChange) - Usas APIs del navegador (
window,localStorage)
En el resto de casos, no. Server Component rinde mejor y accede al backend directamente.
Problema 2: ¿Relación entre layout.js y page.js?
La duda: están en la misma carpeta; ¿quién envuelve a quién?
Solución:
layout.js envuelve page.js y las rutas hijas.
app/
├── layout.js # Envuelve todas las páginas de abajo
├── page.js # Inicio, envuelto por el layout de arriba
└── about/
└── page.js # Acerca de, también envuelto por ese layout
Al cambiar de página, layout.js no se vuelve a renderizar; solo page.js. Por eso la barra de navegación no parpadea.
Problema 3: ¿Cómo obtener parámetros de rutas dinámicas?
La duda: creaste [slug]/page.js pero no sabes leer slug.
Solución:
Usa la prop params:
// app/blog/[slug]/page.js
export default function BlogPost({ params }) {
console.log(params.slug) // El valor de la URL
return <h1>Artículo: {params.slug}</h1>
}
Ruta dinámica anidada, por ejemplo app/blog/[category]/[slug]/page.js:
export default function Post({ params }) {
console.log(params.category, params.slug)
return <h1>{params.category} - {params.slug}</h1>
}
Problema 4: ¿error.js no funciona?
La duda: creaste error.js pero no captura errores del layout.
Solución:
error.js no captura errores del layout.js del mismo nivel. Limitación de React Error Boundary.
Para errores de layout:
- Pon
error.jsen el directorio padre - Usa
global-error.jsen la raíz (debe incluir<html>y<body>)
// app/global-error.js
'use client'
export default function GlobalError({ error, reset }) {
return (
<html>
<body>
<h2>Error global: {error.message}</h2>
<button onClick={reset}>Reintentar</button>
</body>
</html>
)
}
Problema 5: ¿Migrar mi proyecto antiguo?
La duda: App Router trae mucho nuevo y temes reescribir todo.
Solución:
No hay prisa.
Pages Router y App Router pueden coexistir:
- Funciones antiguas en
pages/ - Funciones nuevas en
app/
Vercel ha dicho que Pages Router tendrá soporte a largo plazo; no se abandona.
En un proyecto nuevo, usa App Router directamente: es el futuro y el ecosistema sigue creciendo.
Conclusión
Repaso rápido de los cinco conceptos clave de App Router:
- Enrutamiento por sistema de archivos: la estructura de carpetas es la de rutas;
page.jses la entrada - Server Components: ejecución en servidor por defecto, mejor rendimiento
- Client Components:
'use client'cuando haga falta interactividad - Archivos especiales:
layout.js,loading.js,error.jsdan un proyecto más profesional - Obtención de datos:
async/awaitdirecto, singetServerSideProps
App Router es la dirección de Next.js. Vercel sigue invirtiendo y la comunidad también. Si empiezas ahora, ir a App Router es acertado.
¿Qué sigue?
Pruébalo en código. Crea un proyecto pequeño: un blog o una lista de tareas con App Router. Los conceptos se fijan mejor escribiendo código.
Si te atasacas, no pasa nada: Pages Router y App Router pueden convivir; puedes quedarte en Pages Router un tiempo y migrar poco a poco.
Por último, la documentación oficial de Next.js puede desorientar, pero la parte de App Router está bastante detallada. Para dudas concretas, consulta la doc o busca en GitHub Discussions.
¡Mucho éxito aprendiendo!
FAQ
¿Cuándo debo usar 'use client'?
¿Cuál es la relación entre layout.js y page.js?
¿Cómo obtengo los parámetros de rutas dinámicas?
¿Por qué error.js no puede capturar errores de layout.js?
¿Debo migrar mi proyecto antiguo a App Router?
¿Cuáles son las principales diferencias entre App Router y Pages Router?
12 min de lectura · Publicado el: 18 dic 2025 · Actualizado el: 21 ago 2026
Guía completa de Next.js
Estás leyendo el primer artículo de esta serie. Continúa con el siguiente o abre el hub para ver toda la ruta.



Comentarios
Inicia sesión con GitHub para dejar un comentario