Portafolio profesional

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

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. Estru

Osman Jimenez

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

21 de diciembre de 2025 publicado 4 min lectura 3 temas

Temas

  • 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 (