• Inicio
  • Sobre mí
  • Proyectos
  • Blog
  • Contacto
← Volver al blog

Next.js App Router: guía práctica desde cero

junio 20, 2024

Llevaba tiempo usando Next.js con Pages Router y cuando migré a App Router me costó entender el cambio de paradigma. Aquí te explico lo que aprendí, sin rodeos.

¿Qué cambia con App Router?

El Pages Router funcionaba con archivos en /pages. Cada archivo era una ruta y los datos se obtenían con getServerSideProps o getStaticProps.

App Router cambia todo:

  • Las rutas van en /app
  • Los componentes son Server Components por defecto
  • El data fetching se hace directamente en el componente con async/await
  • Los layouts son reutilizables y anidables

La estructura básica

app/
├── layout.tsx        ← layout raíz (obligatorio)
├── page.tsx          ← ruta /
├── about/
│   └── page.tsx      ← ruta /about
└── blog/
    ├── page.tsx      ← ruta /blog
    └── [slug]/
        └── page.tsx  ← ruta /blog/:slug

Cada carpeta puede tener su propio layout.tsx, loading.tsx y error.tsx.

Server Components vs Client Components

Este es el cambio más importante. Por defecto, todos los componentes son Server Components.

// Server Component — se ejecuta en el servidor
// Puede hacer fetch directo, acceder a DB, etc.
export default async function ProductPage() {
  const products = await fetchProducts() // llamada directa, sin useEffect

  return <ProductList products={products} />
}

Si necesitas interactividad (estado, eventos, hooks), añades 'use client' al principio:

'use client'

import { useState } from 'react'

export default function Counter() {
  const [count, setCount] = useState(0)

  return (
    <button onClick={() => setCount(count + 1)}>
      {count}
    </button>
  )
}

Regla práctica: mantén los Client Components lo más abajo posible en el árbol. Cuantos menos, mejor rendimiento.

Data fetching simplificado

Adiós a getServerSideProps. Ahora haces fetch directamente:

// app/blog/page.tsx
export default async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // revalida cada hora
  }).then(res => res.json())

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Para datos estáticos (como mi blog con MDX), simplemente llamo a la función:

export default function BlogPage() {
  const posts = getAllPosts() // lee archivos MDX en build time

  return <PostList posts={posts} />
}

Loading states nativos

Crea un archivo loading.tsx en cualquier carpeta y Next.js lo muestra automáticamente mientras carga la página:

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 bg-gray-200 rounded w-1/2" />
      <div className="h-4 bg-gray-200 rounded w-full" />
      <div className="h-4 bg-gray-200 rounded w-3/4" />
    </div>
  )
}

No necesitas ningún estado de carga manual. Next.js usa Suspense internamente.

generateStaticParams para rutas dinámicas

Para generar páginas estáticas con rutas dinámicas (como posts de blog):

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = getAllPosts()
  return posts.map(post => ({ slug: post.slug }))
}

export default async function PostPage({
  params
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = getPostBySlug(slug)

  if (!post) notFound()

  return <PostContent post={post} />
}

Next.js genera un HTML por cada slug en build time. Resultado: carga instantánea y buen SEO.

Metadata para SEO

Exporta un objeto metadata o una función generateMetadata:

// Estático
export const metadata = {
  title: 'Mi Blog',
  description: 'Artículos sobre desarrollo web'
}

// Dinámico por página
export async function generateMetadata({ params }) {
  const post = getPostBySlug(params.slug)
  return {
    title: post.title,
    description: post.description,
    openGraph: { type: 'article' }
  }
}

Lo que más me costó entender

Los layouts persisten entre navegaciones. Si tienes un layout con estado, ese estado no se resetea al navegar. Útil para navbars, sidebars y temas.

No puedes importar Server Components desde Client Components (solo al revés). Si tienes un árbol mixto, pasa los Server Components como children.

// ✅ Correcto
export default function ClientWrapper({ children }) {
  return <div onClick={...}>{children}</div>
}

// En el servidor:
<ClientWrapper>
  <ServerComponent />  {/* server component como hijo */}
</ClientWrapper>

¿Vale la pena migrar?

Después de usarlo en este portfolio y en otros proyectos, sí. Las ventajas son claras:

  • Menos JavaScript en el cliente
  • Data fetching más simple y directo
  • Layouts anidados sin repetir código
  • Loading states automáticos

La curva de aprendizaje existe, pero una vez que interiorices el modelo mental de Server vs Client, todo encaja.

Si tienes dudas sobre algún concepto, escríbeme.

← Volver a todos los posts