---
name: nextjs-patrones
description: >
  Patrones Next.js App Router: nested layouts, parallel routes, intercepting
  routes, caching strategies, Partial Prerendering y autenticación con middleware.
  Cargar cuando se diseñe arquitectura de una app Next.js o se optimice caching.
version: "1.0.0"
herramientasPermitidas: [Read, Glob, Grep]
exclusiones:
  - "No cargar para implementar un endpoint o página específica — para implementación concreta cargar `nextjs-experto`; este skill es para diseño de arquitectura y estrategia de caching."
  - "No cargar para proyectos con Pages Router — los patrones de nested layouts, parallel routes y PPR son exclusivos del App Router."
  - "No cargar para tests de páginas Next.js — para testing cargar `nextjs-testing`."
  - "No cargar para errores de build o hydration — esos son errores de plataforma; cargar `build-errors-nextjs`."
evolvable: true  # default para skill estandar
---
# Next.js Patrones — Arquitectura App Router

El App Router introduce patrones de composición que no existían en Pages Router.
Este skill cubre las decisiones de arquitectura: cuándo usar layouts anidados,
cómo aprovechar parallel routes para UI compleja, qué estrategia de cache elegir
para cada tipo de dato, y cómo estructurar autenticación con middleware.

## Cuándo cargar este skill

Invoca `Skill("nextjs-patrones")` cuando:

- Se diseñe la estructura de rutas de una app Next.js nueva
- Se implemente un modal que debe mantener la ruta anterior (intercepting routes)
- Se optimice el comportamiento de cache de páginas o datos
- Se configure autenticación o i18n con middleware
- Se evalúen las estrategias de rendering: SSR, SSG, ISR, PPR

## Cuándo NO cargar

- La pregunta es sobre implementar un endpoint o página específica — para implementación concreta cargar `nextjs-experto`; este skill cubre decisiones de arquitectura y estrategia.
- El proyecto usa Pages Router — los patrones de nested layouts, parallel routes y PPR son exclusivos del App Router.
- La pregunta es sobre tests de páginas Next.js — para testing cargar `nextjs-testing`.
- La pregunta es sobre errores de build o hydration — esos son errores de plataforma; cargar `build-errors-nextjs`.

## Conceptos clave

### Nested Layouts y composición

Cada segmento de ruta puede tener su propio `layout.tsx`. Los layouts se anidan:
el layout de `/dashboard` envuelve a todos los layouts y páginas bajo esa ruta.
Los layouts son Server Components y persisten entre navegaciones del mismo segmento.

### Route Groups

Los route groups (carpetas entre paréntesis como `(auth)`) agrupan rutas sin
afectar la URL. Sirven para aplicar layouts diferentes a segmentos del mismo
nivel, o para organizar el código sin impactar las rutas públicas.

### Parallel Routes y Slots

Los slots (`@nombre`) permiten renderizar múltiples páginas en el mismo layout
simultáneamente. El layout recibe los slots como props. Útil para dashboards
con secciones independientes o vistas divididas.

## Reglas obligatorias

### Layout solo para UI que persiste — no para datos dinámicos por usuario

```tsx
// MAL — el layout hace fetch de datos que dependen del usuario
// Los layouts se cachean y puede servirse el mismo a distintos usuarios
export default async function DashboardLayout({ children }) {
    const usuario = await obtenerUsuarioActual(); // PELIGROSO en layout
    return <aside>{usuario.nombre}</aside>;
}

// BIEN — datos dinamicos en el page.tsx o en un Client Component
// con fetch propio que respeta el contexto de la request
export default function DashboardLayout({ children }) {
    return (
        <div className="flex">
            <NavegacionLateral /> {/* Server Component con su propio fetch */}
            <main>{children}</main>
        </div>
    );
}
```

### Cache con tags para invalidación granular — nunca `no-store` por comodidad

```tsx
// MAL — deshabilitar cache para evitar pensar en invalidacion
const productos = await fetch('/api/productos', { cache: 'no-store' });

// BIEN — cache con revalidacion y tags especificos
const productos = await fetch('/api/productos', {
    next: {
        revalidate: 3600,          // Revalidar cada hora como maximo
        tags: ['productos'],        // Invalidar con revalidateTag('productos')
    },
});

// En el Server Action al mutar:
revalidateTag('productos');        // Solo invalida lo necesario
```

### Composition pattern: Server Component envuelve Client Component

```tsx
// Server Component puede pasar datos como props a Client Components
// pero NO puede importar un Client Component que use hooks en el servidor
// BIEN — composicion correcta
export default async function PaginaProducto({ params }) {
    const producto = await obtenerProducto(params.id); // Server
    return (
        <div>
            <DetalleProducto producto={producto} />      {/* Server */}
            <BotonAgregarCarrito productoId={producto.id} /> {/* Client */}
        </div>
    );
}
```

## Patrones recomendados

### Parallel Routes para dashboard con secciones independientes

```
app/dashboard/
├── layout.tsx         # Recibe @analytics y @equipo como props
├── page.tsx
├── @analytics/
│   └── page.tsx       # Carga independientemente
└── @equipo/
    └── page.tsx       # Carga independientemente
```

```tsx
// layout.tsx
export default function DashboardLayout({
    children,
    analytics,
    equipo,
}: {
    children: React.ReactNode;
    analytics: React.ReactNode;
    equipo: React.ReactNode;
}) {
    return (
        <div className="grid grid-cols-3 gap-4">
            <main className="col-span-2">{children}</main>
            <aside>
                <Suspense fallback={<Skeleton />}>{analytics}</Suspense>
                <Suspense fallback={<Skeleton />}>{equipo}</Suspense>
            </aside>
        </div>
    );
}
```

### Intercepting Routes para modales con URL propia

```
app/
├── productos/
│   ├── page.tsx                  # /productos — lista
│   ├── [id]/page.tsx             # /productos/[id] — página completa
│   └── (.)foto/[id]/page.tsx    # Intercepta /foto/[id] desde /productos
└── foto/
    └── [id]/page.tsx             # /foto/[id] — página completa si acceso directo
```

El `(.)` intercepta rutas en el mismo nivel. `(..)` intercepta un nivel arriba.
Al navegar desde `/productos`, abre el modal. Al acceder directo a `/foto/123`,
muestra la página completa — permite compartir URLs de modales.

### Estrategias de cache por tipo de contenido

```tsx
// Contenido estático (documentación, landing pages)
// No especificar cache — Next.js cachea indefinidamente en build
export default async function PaginaMarketing() { ... }

// Contenido con actualizacion periodica (catalogo, precios)
async function obtenerCatalogo() {
    return fetch('/api/catalogo', { next: { revalidate: 3600 } });
}

// Contenido personalizado por usuario (carrito, perfil)
async function obtenerCarrito(usuarioId: string) {
    return fetch(`/api/carrito/${usuarioId}`, { cache: 'no-store' });
}

// Contenido en tiempo real (stock, precios live)
export const dynamic = 'force-dynamic'; // en el page.tsx
```

### Edge Runtime para middleware de autenticación

```tsx
// middleware.ts
export const runtime = 'edge'; // más rápido, sin cold start

export async function middleware(request: NextRequest) {
    const token = request.cookies.get('session')?.value;

    if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
        const loginUrl = new URL('/login', request.url);
        loginUrl.searchParams.set('callbackUrl', request.nextUrl.pathname);
        return NextResponse.redirect(loginUrl);
    }

    // Agregar headers de usuario para Server Components downstream
    const response = NextResponse.next();
    response.headers.set('x-user-id', decodeToken(token)?.id ?? '');
    return response;
}

export const config = {
    matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
};
```

### Partial Prerendering (PPR) — Next.js 14+

```tsx
// next.config.ts
export default {
    experimental: { ppr: true },
};

// page.tsx — shell estático + partes dinámicas en Suspense
export default function PaginaProducto({ params }) {
    return (
        <>
            {/* Estático — se precompila */}
            <HeaderProducto />
            <DescripcionProducto id={params.id} />

            {/* Dinámico — se hace streaming por separado */}
            <Suspense fallback={<SkeletonStock />}>
                <StockEnTiempoReal id={params.id} />
            </Suspense>
        </>
    );
}
```

## Anti-patrones conocidos

### Prop drilling de Server Components a través de múltiples niveles

```tsx
// MAL — pasar props a través de 4 niveles de Server Components
<Layout usuario={usuario}>
  <Dashboard usuario={usuario}>
    <Header usuario={usuario} />

// BIEN — cada Server Component hace su propio fetch o usa Context del lado cliente
// Los Server Components pueden colocarse directamente en el árbol
```

### Route Handler para todo cuando Server Actions son suficientes

Para mutaciones simples desde formularios o acciones de usuario, los Server Actions
son más simples que crear un route handler separado. Los route handlers tienen
sentido para endpoints que consumirán terceros, webhooks o necesitan headers HTTP.

### Imports de librerias pesadas en el bundle del cliente

```tsx
// MAL — lodash completo en el cliente
"use client";
import _ from 'lodash';

// BIEN — importar solo lo necesario, o mover la lógica al servidor
import debounce from 'lodash/debounce';
// O mejor: la transformación de datos en un Server Component
```

## Gotchas / Errores comunes no obvios

**`loading.tsx` en un segmento bloquea el loading de segmentos padre**: en una ruta `/dashboard/pedidos`, si `pedidos/loading.tsx` existe pero el fetch lento está en el layout de `/dashboard`, el spinner de pedidos aparece pero la UI de dashboard también queda pendiente. Causa: el `loading.tsx` solo envuelve la página del segmento donde vive, no los layouts padres. Fix: poner el `loading.tsx` al mismo nivel que el layout o componente que hace el fetch lento.

**Parallel routes con `@slot` que no tienen `default.tsx` causan 404 en navegación directa**: al navegar directamente a una URL (refresh o link externo) con parallel routes, Next.js necesita renderizar todos los slots — si alguno no tiene `default.tsx`, lanza 404. Causa: en navegación suave (client-side), el estado de los slots se mantiene; en navegación directa, Next.js busca `default.tsx` como fallback. Fix: crear `default.tsx` en cada `@slot` que no tenga una ruta correspondiente.

**`revalidateTag()` en Server Action no invalida el cache si el `fetch` no declaró el tag**: se llama `revalidateTag('productos')` pero la página no se refresca. Causa: el `fetch` correspondiente no tenía `next: { tags: ['productos'] }` — sin tag declarado, no hay entrada en el cache que invalidar. Fix: verificar que cada `fetch` que debe invalidarse declara explícitamente los tags con `next: { tags: ['nombre-del-tag'] }`.

**Intercepting routes `(..)ruta` no intercepta si el layout padre no coincide**: al configurar `(..)modal` para interceptar `/fotos/[id]` desde `/`, la intercepción no funciona si el archivo de intercepción está un nivel más arriba de lo esperado. Causa: la notación `(.)` intercepta el mismo segmento, `(..)` intercepta un nivel arriba, `(...)` intercepta desde la raíz — un error de conteo de puntos hace que la intercepción no se active y navega a la página completa. Fix: contar los segmentos de ruta entre el origen y el destino para elegir la notación correcta.

## Checklist de verificación

- [ ] Layouts solo contienen UI persistente, sin datos dinámicos por usuario
- [ ] Cache configurada con tags para invalidación granular
- [ ] Parallel routes con Suspense independiente por slot
- [ ] Intercepting routes cuando se necesitan modales con URL compartible
- [ ] Middleware en Edge Runtime para latencia mínima
- [ ] Server Components hacen fetch directamente — no prop drilling de datos
- [ ] PPR evaluado para páginas con mezcla de contenido estático y dinámico

## Referencias

- [Next.js Routing](https://nextjs.org/docs/app/building-your-application/routing)
- [Next.js Caching Deep Dive](https://nextjs.org/docs/app/building-your-application/caching)
- [Partial Prerendering](https://nextjs.org/docs/app/api-reference/next-config-js/ppr)

---
*Skill creado con swl:crear-skill el 2026-03-31. Versión 1.0.0.*
