Data fetching y cache en Next.js (App Router)

1. Descripción general

Next.js App Router cachea automáticamente los resultados de fetch en el servidor para reducir la carga del backend y acelerar las páginas. Pero este mecanismo de caché es completamente diferente al caché HTTP tradicional que conoces, y si no lo entiendes bien, es fácil encontrarse en situaciones donde “los datos antiguos no se actualizan” o al contrario, “cada página se renderiza de nuevo desde cero, terriblemente lenta”.

Este artículo recorre las capas de caché de Next.js: el caché de fetch, revalidación por tiempo, revalidación por etiqueta, y renderizado dinámico. Después de leerlo, sabrás elegir la estrategia de fetch correcta para cada tipo de página (estática, ISR, dinámica) y podrás depurar cuando el caché “se pone terco”.

Nota: este artículo aplica al App Router (Next.js 13+, se recomienda 14/15). El Pages Router usa un mecanismo diferente (getStaticProps, getServerSideProps) que no está cubierto en este artículo.

2. Las cuatro capas de caché de Next.js

Next.js tiene 4 mecanismos de caché distintos, fáciles de confundir si los mezclas:

Mecanismo Ubicación Propósito
Request Memoization Servidor, en una renderización Agrupa múltiples llamadas fetch idénticas en la misma solicitud
Data Cache Servidor, entre solicitudes Cachea resultados de fetch a largo plazo, puede revalidarse
Full Route Cache Servidor, en build time Cachea HTML + payload RSC de rutas estáticas
Router Cache Cliente, en la sesión Cachea resultados de navegación para retroceso/avance rápido

Este artículo se enfoca en el Data Cache porque es lo que controlas directamente a través de las opciones de fetch.

3. Fetch por defecto: caché “indefinido”

En un Server Component, fetch es parchado por Next.js para cachear automáticamente:

// app/products/page.tsx
async function getProducts() {
  const res = await fetch("https://api.shop.example.com/products");
  if (!res.ok) throw new Error("No se pudo llamar a la API de productos");
  return res.json();
}

export default async function ProductsPage() {
  const products = await getProducts();
  return (
    <ul>
      {products.map((p: { id: string; name: string }) => (
        <li key={p.id}>{p.name}</li>
      ))}
    </ul>
  );
}
  • Si fetch no especifica cache → Next.js usa force-cache por defecto.
  • El resultado se guarda en Data Cache, se reutiliza en todas las solicitudes posteriores, sin expiración a menos que revalides.
  • Esta página se coloca en Full Route Cache: se genera HTML estático, se sirve como página estática.

Esta es la razón por la que muchas personas se sorprenden: actualizan datos en el backend pero la página no cambia — porque Next.js la trata como datos estáticos.

4. Controlar el caché con la opción cache

Si quieres obtener siempre datos frescos, usa no-store:

async function getOrderStatus(orderId: string) {
  const res = await fetch(
    `https://api.shop.example.com/orders/${orderId}`,
    { cache: "no-store" }
  );
  return res.json();
}
  • cache: "no-store" desactiva completamente Data Cache para esta solicitud.
  • La ruta que contiene este fetch se marcará como dinámica, se renderiza en cada solicitud.
  • Apropiado para datos que cambian constantemente: pedidos en procesamiento, precios en tiempo real, paneles.

Nota: solo se necesita un fetch con no-store en una página para que toda esa página se convierta en renderizado dinámico — incluso los otros fetch en la misma página aún usan force-cache.

5. Revalidar por tiempo (ISR)

Para cachear pero actualizar automáticamente después de N segundos (Regeneración Estática Incremental), usa next.revalidate:

async function getCategories() {
  const res = await fetch(
    "https://api.shop.example.com/categories",
    { next: { revalidate: 300 } } // 5 minutos
  );
  return res.json();
}
  • revalidate: 300 significa: los datos se consideran “antiguos” después de 300 segundos.
  • La primera solicitud después del período de 300s aún devuelve datos antiguos (stale-while-revalidate) pero Next.js dispara un fetch nuevo en segundo plano.
  • Las siguientes solicitudes reciben datos nuevos.

Puedes aplicarlo a toda la ruta exportando una variable al inicio del archivo:

// app/blog/[slug]/page.tsx
export const revalidate = 300;

export default async function BlogPost({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.blog.example.com/posts/${params.slug}`);
  return <article>{/* ... */}</article>;
}
  • export const revalidate aplica a todos los fetch en la ruta si ese fetch no declara un revalidate propio diferente.
  • Si un fetch específico declara un next.revalidate diferente, ese valor toma precedencia para ese fetch.

6. Revalidar por etiqueta: revalidateTag

La revalidación basada en tiempo funciona bien para contenido que cambia regularmente, pero no es ideal cuando quieres actualizar justo cuando los datos cambian (por ejemplo, un administrador acaba de actualizar el precio de un producto). Aquí es donde usas etiquetas de caché.

// app/products/[id]/page.tsx
async function getProduct(id: string) {
  const res = await fetch(
    `https://api.shop.example.com/products/${id}`,
    { next: { tags: [`product-${id}`] } }
  );
  return res.json();
}

Cuando un administrador actualiza un producto, llama a revalidateTag en un Route Handler o Server Action:

// app/actions/update-product.ts
"use server";

import { revalidateTag } from "next/cache";

export async function updateProduct(id: string, formData: FormData) {
  await fetch(`https://api.shop.example.com/products/${id}`, {
    method: "PATCH",
    body: formData,
  });

  revalidateTag(`product-${id}`); // elimina caché con esta etiqueta
}
  • tags asigna una “etiqueta” a la entrada de caché del fetch.
  • revalidateTag(tag) elimina toda entrada de Data Cache que tenga esa etiqueta, sin importar de qué ruta venga.
  • El siguiente acceso hará un fetch nuevo de datos frescos y los cacheará con la nueva etiqueta.

Este es el mecanismo más poderoso porque desvincula la lógica “cuándo invalidar” de “qué página muestra los datos” — es como cache tagging en Spring Cache o Redis.

7. Revalidar por ruta: revalidatePath

Si no usas etiquetas, puedes revalidar directamente por la ruta:

"use server";

import { revalidatePath } from "next/cache";

export async function publishPost(slug: string) {
  // ... llama a la API para publicar ...
  revalidatePath(`/blog/${slug}`);   // solo esta página
  revalidatePath("/blog", "page");    // o todo el listado
}
  • revalidatePath("/blog/[slug]") elimina Full Route Cache + Data Cache de exactamente esa ruta.
  • El segundo parámetro ("page" o "layout") decide si también revalidar rutas hijas que usan ese layout.
  • Diferente a revalidateTag: revalidatePath se vincula a una ruta específica, mientras que una etiqueta puede usarse en múltiples rutas diferentes.

Nota: revalidateTag/revalidatePath solo funcionan dentro de un Server Action o Route Handler, no se pueden llamar directamente desde un Client Component.

8. Renderizado dinámico: cuando el caché pierde sentido

Algunas APIs hacen que Next.js automáticamente convierta la ruta a dinámica, saltándose Full Route Cache:

import { cookies, headers } from "next/headers";

export default async function AccountPage() {
  const sessionToken = (await cookies()).get("session")?.value;
  const userAgent = (await headers()).get("user-agent");

  const res = await fetch("https://api.shop.example.com/me", {
    headers: { Authorization: `Bearer ${sessionToken}` },
    cache: "no-store",
  });

  return <div>Hola, {(await res.json()).name}</div>;
}
  • Llamar a cookies() o headers() automáticamente marca la ruta como dinámica — tiene sentido porque los datos dependen del usuario específico.
  • Combinado con cache: "no-store" asegura que los datos siempre sean frescos según la sesión.

Fuerza renderizado dinámico explícitamente (cuando sabes que no puedes cachear):

export const dynamic = "force-dynamic";

9. Route Handlers también tienen su propio caché

Un Route Handler (app/api/.../route.ts) con método GET también sigue las mismas reglas de caché que un Server Component:

// app/api/stock/route.ts
export const revalidate = 60;

export async function GET() {
  const res = await fetch("https://api.shop.example.com/stock");
  const data = await res.json();
  return Response.json(data);
}
  • Un GET que no usa el objeto Request (no lee request.url para obtener query) podría cachearse estáticamente.
  • Si el handler usa request.headers, request.cookies, u otro método que no sea GET (POST, PUT…) → siempre dinámico, sin caché.

10. Client Component: el caché pertenece a otra librería

El caché de fetch de Next.js solo aplica en el servidor. En Client Components, haces fetch tú mismo con useEffect o usas una librería de caché propia como SWR:

npm install swr
"use client";

import useSWR from "swr";

const fetcher = (url: string) => fetch(url).then((r) => r.json());

export function CartBadge({ userId }: { userId: string }) {
  const { data, isLoading } = useSWR(`/api/cart/${userId}`, fetcher, {
    refreshInterval: 10_000, // sondea cada 10s
  });

  if (isLoading) return <span>...</span>;
  return <span>{data.itemCount} artículos</span>;
}
  • SWR tiene caché, deduplicación de solicitudes, revalidación al cambiar de pestaña — completamente independiente del Data Cache del servidor.
  • Úsalo cuando necesites datos actualizándose constantemente en el cliente sin recargar toda la página.

11. Escribir pruebas para la lógica relacionada con claves de caché

El propio mecanismo de caché de Next.js es difícil de hacer unit test directamente, pero la lógica de generar etiquetas/rutas suele valer la pena hacer test por separado — evita errores de tipografía en etiquetas que hagan que revalidateTag no coincida.

// lib/cache-keys.ts
export function productTag(id: string): string {
  return `product-${id}`;
}

export function categoryTag(id: string): string {
  return `category-${id}`;
}
// lib/cache-keys.test.ts
import { describe, it, expect } from "vitest";
import { productTag, categoryTag } from "./cache-keys";

describe("cache-keys", () => {
  it("genera etiqueta de producto en formato correcto", () => {
    expect(productTag("sku-123")).toBe("product-sku-123");
  });

  it("etiqueta de producto y categoría no se solapan", () => {
    const id = "42";
    expect(productTag(id)).not.toBe(categoryTag(id));
  });
});
  • Separar la función generadora de etiquetas en su propio módulo asegura que el lugar donde llamas a fetch (asignando etiqueta) y donde llamas a revalidateTag (eliminando etiqueta) siempre usen el mismo formato.
  • Este es un error real muy común: un lado escribe product-${id}, el otro escribe products-${id} → la revalidación nunca coincide.
✓ cache-keys > genera etiqueta de producto en formato correcto
✓ cache-keys > etiqueta de producto y categoría no se solapan

12. Errores comunes

  • Olvidar que fetch cachea indefinidamente por defecto: despliegas y los datos no cambian porque aún usas caché antiguo del build anterior (con static export) o del primer fetch (con despliegue en servidor). Corrígelo con revalidate o no-store.
  • Mezclar no-store y caché innecesariamente en el mismo componente: solo un fetch con no-store hace toda la página dinámica, pierdes los beneficios de caché de otros fetch.
  • Llamar a revalidateTag/revalidatePath fuera de un Server Action/Route Handler: no tiene efecto, Next.js lanzará un error o lo ignorará silenciosamente según la versión.
  • Usar fetch normal (no parchado por Next.js) dentro de una librería de terceros (por ejemplo SDK que usa axios o undici puros): no es administrado por Data Cache, debes revalidar de otra forma (por ejemplo caché en otra capa o crear un wrapper que llame a fetch estándar).
  • No etiquetar por entidad específica: asignar etiqueta muy genérica ("products" para todos los productos) hace que una pequeña actualización invalide todo el caché, pierdes los beneficios de caché por artículo.

Cuándo NO cachear: datos personalizados por sesión (carrito, saldo, notificaciones), datos en tiempo real (precio de bolsa, estado de pago), o cualquier respuesta que dependa de cookies()/headers(). Estos casos deben usar no-store o force-dynamic desde el inicio, no intentes forzar caché y luego revalidar constantemente — es complejo y no respeta la naturaleza de los datos.

13. Conclusión

El caché en Next.js App Router tiene múltiples capas, pero la parte que controlas más activamente es Data Cache a través de opciones de fetch: cache, next.revalidate, next.tags. Regla rápida para elegir: datos que cambian poco → revalidate por tiempo; datos que cambian según eventos específicos (admin actualiza) → asigna tags y usa revalidateTag; datos por usuario o en tiempo real → no-store/force-dynamic. Dominando estos tres grupos, raramente necesitarás adivinar “por qué los datos no se actualizan”.

nextjsreactcachingapp-routerserver-components