Next.js 14: Guía Completa del App Router

21 de diciembre de 2025
Osman Jimenez
Next.js React Desarrollo Web

El Nuevo App Router de Next.js

Next.js 14 introduce el App Router, una nueva forma de construir aplicaciones con React Server Components, streaming y más.

Estructura de Carpetas

app/
├── layout.tsx          # Layout raíz
├── page.tsx            # Página principal
├── loading.tsx         # UI de carga
├── error.tsx           # UI de error
├── not-found.tsx       # 404
├── blog/
│   ├── layout.tsx
│   ├── page.tsx
│   └── [slug]/
│       └── page.tsx
└── api/
    └── users/
        └── route.ts

Server Components por Defecto

// app/page.tsx - Server Component
export default async function HomePage() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json());
  
  return (
    

Blog Posts

{posts.map(post => (

{post.title}

{post.excerpt}

))}
); }

Client Components

'use client'; // Directiva para Client Component

import { useState } from 'react';

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

Layouts Anidados

// app/layout.tsx - Root Layout
export default function RootLayout({ children }) {
  return (
    
      
        
Header Global
{children}
Footer Global
); } // app/blog/layout.tsx - Blog Layout export default function BlogLayout({ children }) { return (
{children}
); }

Data Fetching

// Fetch con cache
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'force-cache' // Default
  });
  return res.json();
}

// Revalidar cada 60 segundos
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }
  });
  return res.json();
}

// Sin cache
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    cache: 'no-store'
  });
  return res.json();
}

Rutas Dinámicas

// app/blog/[slug]/page.tsx
export default async function BlogPost({ params }) {
  const post = await getPost(params.slug);
  
  return (
    

{post.title}

); } // Generar rutas estáticas export async function generateStaticParams() { const posts = await getPosts(); return posts.map((post) => ({ slug: post.slug })); }

Loading y Streaming

// app/blog/loading.tsx
export default function Loading() {
  return 
Cargando posts...
; } // Streaming con Suspense import { Suspense } from 'react'; export default function Page() { return (

Mi Blog

Cargando posts...
}>
); }

Error Handling

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

export default function Error({ error, reset }) {
  return (
    

Algo salió mal!

{error.message}

); }

API Routes

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

export async function GET(request: Request) {
  const users = await getUsers();
  return NextResponse.json(users);
}

export async function POST(request: Request) {
  const body = await request.json();
  const user = await createUser(body);
  return NextResponse.json(user, { status: 201 });
}

// app/api/users/[id]/route.ts
export async function GET(
  request: Request,
  { params }: { params: { id: string } }
) {
  const user = await getUser(params.id);
  return NextResponse.json(user);
}

Metadata para SEO

// Metadata estática
export const metadata = {
  title: 'Mi Blog',
  description: 'Un blog increíble',
  openGraph: {
    title: 'Mi Blog',
    description: 'Un blog increíble',
    images: ['/og-image.jpg']
  }
};

// Metadata dinámica
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.image]
    }
  };
}

Server Actions

'use server';

export async function createPost(formData: FormData) {
  const title = formData.get('title');
  const content = formData.get('content');
  
  await db.post.create({
    data: { title, content }
  });
  
  revalidatePath('/blog');
  redirect('/blog');
}

// Uso en Client Component
'use client';

import { createPost } from './actions';

export default function NewPostForm() {
  return (