Next.js 14 App Router: Guía Completa

Todo lo que necesitas saber sobre el App Router de Next.js 14: Server Components, streaming, y más

Pedro Moya5 min read
Compartir:

Introducción

Next.js 14 introduce mejoras significativas al App Router, haciéndolo la opción recomendada para nuevos proyectos.

¿Qué es el App Router?

El App Router es el nuevo sistema de routing basado en la carpeta app/:

app/
├── layout.tsx          # Root layout
├── page.tsx            # Homepage (/)
├── blog/
│   ├── layout.tsx      # Blog layout
│   ├── page.tsx        # Blog index (/blog)
│   └── [slug]/
│       └── page.tsx    # Post page (/blog/[slug])
└── api/
    └── hello/
        └── route.ts    # API route (/api/hello)

Server Components por Defecto

Todos los componentes son Server Components por defecto:

// app/page.tsx
// Este es un Server Component (por defecto)
export default async function HomePage() {
  // Puedes hacer fetch directamente
  const data = await fetch('https://api.example.com/data')
  const json = await data.json()

  return (
    <div>
      <h1>Welcome</h1>
      <pre>{JSON.stringify(json, null, 2)}</pre>
    </div>
  )
}

Client Components

Usa 'use client' cuando necesites interactividad:

'use client'

import { useState } from 'react'

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

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

Layouts y Templates

Layouts

Los layouts envuelven páginas y persisten entre navegaciones:

// app/layout.tsx
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="es">
      <body>
        <nav>Navigation</nav>
        {children}
        <footer>Footer</footer>
      </body>
    </html>
  )
}

Nested Layouts

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <div className="blog-container">
      <aside>Sidebar</aside>
      <main>{children}</main>
    </div>
  )
}

Routing Avanzado

Dynamic Routes

// app/blog/[slug]/page.tsx
interface PageProps {
  params: {
    slug: string
  }
}

export default function BlogPost({ params }: PageProps) {
  return <h1>Post: {params.slug}</h1>
}

// Generate static params (SSG)
export async function generateStaticParams() {
  const posts = await getPosts()

  return posts.map((post) => ({
    slug: post.slug,
  }))
}

Catch-all Routes

// app/docs/[...slug]/page.tsx
interface PageProps {
  params: {
    slug: string[]
  }
}

export default function Docs({ params }: PageProps) {
  // /docs/a/b/c -> params.slug = ['a', 'b', 'c']
  return <h1>Docs: {params.slug.join('/')}</h1>
}

Optional Catch-all Routes

// app/shop/[[...slug]]/page.tsx
// Matches:
// - /shop
// - /shop/clothes
// - /shop/clothes/tops

Loading y Error States

Loading UI

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

Error Boundary

// app/blog/error.tsx
'use client'

export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>Try again</button>
    </div>
  )
}

Data Fetching

Server Components

// Fetch en Server Component
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    // Opciones de cache
    next: { revalidate: 3600 }, // ISR cada hora
  })

  if (!res.ok) {
    throw new Error('Failed to fetch data')
  }

  return res.json()
}

export default async function Page() {
  const data = await getData()
  return <div>{JSON.stringify(data)}</div>
}

Parallel Data Fetching

export default async function Page() {
  // Estas peticiones se ejecutan en paralelo
  const [userData, postsData] = await Promise.all([
    fetch('https://api.example.com/user').then((r) => r.json()),
    fetch('https://api.example.com/posts').then((r) => r.json()),
  ])

  return (
    <div>
      <UserProfile data={userData} />
      <PostsList data={postsData} />
    </div>
  )
}

Sequential Data Fetching

export default async function Page() {
  // Primero obtiene el usuario
  const user = await fetch(`https://api.example.com/user/1`).then((r) =>
    r.json()
  )

  // Luego usa el ID del usuario
  const posts = await fetch(
    `https://api.example.com/posts?userId=${user.id}`
  ).then((r) => r.json())

  return <div>{/* ... */}</div>
}

Metadata API

Static Metadata

// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Blog Post',
  description: 'This is my awesome blog post',
}

export default function Page() {
  return <article>...</article>
}

Dynamic Metadata

interface PageProps {
  params: { slug: string }
}

export async function generateMetadata({
  params,
}: PageProps): Promise<Metadata> {
  const post = await getPost(params.slug)

  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [post.image],
    },
  }
}

export default async function Page({ params }: PageProps) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

Route Handlers (API Routes)

// app/api/posts/route.ts
import { NextResponse } from 'next/server'

export async function GET(request: Request) {
  const posts = await getPosts()

  return NextResponse.json({ posts })
}

export async function POST(request: Request) {
  const data = await request.json()
  const post = await createPost(data)

  return NextResponse.json({ post }, { status: 201 })
}

Dynamic Route Handlers

// app/api/posts/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const post = await getPost(params.id)

  if (!post) {
    return new NextResponse('Not found', { status: 404 })
  }

  return NextResponse.json({ post })
}

Streaming y Suspense

import { Suspense } from 'react'

async function SlowComponent() {
  await new Promise((resolve) => setTimeout(resolve, 3000))
  return <div>Slow data loaded!</div>
}

export default function Page() {
  return (
    <div>
      <h1>Fast content here</h1>

      <Suspense fallback={<div>Loading slow component...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  )
}

Conclusión

El App Router de Next.js 14 ofrece:

✅ Server Components por defecto ✅ Streaming y Suspense built-in ✅ Layouts anidados ✅ Loading y error states automáticos ✅ Metadata API mejorada ✅ Route Handlers tipados

Es más potente y flexible que el Pages Router, con mejor performance out-of-the-box.

Recursos


¿Preguntas sobre el App Router? Contáctame.