Cambiar tema

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

Easton editorial illustration: one large folder tree unfolding into a route map

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/await dentro 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:

  1. Pon error.js en el directorio padre
  2. Usa global-error.js en 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:

  1. Enrutamiento por sistema de archivos: la estructura de carpetas es la de rutas; page.js es la entrada
  2. Server Components: ejecución en servidor por defecto, mejor rendimiento
  3. Client Components: 'use client' cuando haga falta interactividad
  4. Archivos especiales: layout.js, loading.js, error.js dan un proyecto más profesional
  5. Obtención de datos: async/await directo, sin getServerSideProps

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'?
Solo cuando necesites interactividad: usar hooks de React (useState, useEffect), manejar interacciones del usuario (onClick, onChange) o usar APIs del navegador (window, localStorage). Por defecto, Server Component ofrece mejor rendimiento.
¿Cuál es la relación entre layout.js y page.js?
layout.js envuelve page.js y las rutas hijas. Al cambiar de página, layout.js no se vuelve a renderizar; solo se actualiza page.js, así que elementos compartidos como la barra de navegación no parpadean.
¿Cómo obtengo los parámetros de rutas dinámicas?
A través de la prop params del componente. Por ejemplo, en app/blog/[slug]/page.js, usa el parámetro { params } y accede al valor slug de la URL con params.slug.
¿Por qué error.js no puede capturar errores de layout.js?
Es una limitación de React Error Boundary: solo captura errores de componentes hijos, no de componentes hermanos o padres. Para capturar errores de layout, coloca error.js en el directorio padre o usa global-error.js en la raíz.
¿Debo migrar mi proyecto antiguo a App Router?
No hace falta migrar de inmediato. Pages Router y App Router pueden coexistir: las funciones antiguas siguen en pages/ y las nuevas en app/. Vercel ha prometido soporte a largo plazo para Pages Router. En proyectos nuevos, conviene usar App Router directamente.
¿Cuáles son las principales diferencias entre App Router y Pages Router?
App Router se basa en Server Components, con renderizado en servidor por defecto y mejor rendimiento; usa enrutamiento por sistema de archivos y admite layouts anidados; la obtención de datos se hace con async/await directamente. Pages Router usa principalmente componentes de cliente y requiere getServerSideProps para obtener datos.

12 min de lectura · Publicado el: 18 dic 2025 · Actualizado el: 21 ago 2026

Comentarios

Inicia sesión con GitHub para dejar un comentario

Easton BlogEaston Blog