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.