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
fetchno especificacache→ Next.js usaforce-cachepor 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
fetchconno-storeen una página para que toda esa página se convierta en renderizado dinámico — incluso los otrosfetchen la misma página aún usanforce-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: 300significa: 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 revalidateaplica a todos los fetch en la ruta si ese fetch no declara unrevalidatepropio diferente.- Si un fetch específico declara un
next.revalidatediferente, 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
}
tagsasigna 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:revalidatePathse vincula a una ruta específica, mientras que una etiqueta puede usarse en múltiples rutas diferentes.
Nota:
revalidateTag/revalidatePathsolo 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()oheaders()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
GETque no usa el objetoRequest(no leerequest.urlpara obtener query) podría cachearse estáticamente. - Si el handler usa
request.headers,request.cookies, u otro método que no seaGET(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 arevalidateTag(eliminando etiqueta) siempre usen el mismo formato. - Este es un error real muy común: un lado escribe
product-${id}, el otro escribeproducts-${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
revalidateono-store. - Mezclar
no-storey caché innecesariamente en el mismo componente: solo un fetch conno-storehace toda la página dinámica, pierdes los beneficios de caché de otros fetch. - Llamar a
revalidateTag/revalidatePathfuera de un Server Action/Route Handler: no tiene efecto, Next.js lanzará un error o lo ignorará silenciosamente según la versión. - Usar
fetchnormal (no parchado por Next.js) dentro de una librería de terceros (por ejemplo SDK que usaaxiosoundicipuros): no es administrado por Data Cache, debes revalidar de otra forma (por ejemplo caché en otra capa o crear un wrapper que llame afetchestá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 usarno-storeoforce-dynamicdesde 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”.