Cuando trabajas con Next.js y aplicaciones que consumen datos de APIs externas, mantener el contenido actualizado se convierte en un desafío. Tienes contenido que cambia en tu backend, pero tu frontend sigue mostrando datos en caché que ya no son relevantes.
La solución tradicional sería reducir el tiempo de caché o deshabilitarlo completamente, pero eso afecta significativamente el rendimiento. La alternativa es implementar revalidación on-demand o revalidación bajo demanda, un mecanismo que permite invalidar el caché de forma programática cuando el contenido cambia.
En este artículo exploraremos cómo implementar un sistema de revalidación de caché en Next.js que se actualice automáticamente cuando cambies contenido en tu backend, sin sacrificar el rendimiento.
¿Qué es la Revalidación de Caché?
La revalidación de caché es un mecanismo que permite invalidar el caché de Next.js de forma programática, en lugar de esperar a que expire el tiempo de revalidación configurado.
Next.js ofrece dos formas principales de hacer esto:
revalidatePath: Invalida el caché de una ruta específica (por ejemplo,/post/mi-articulo)revalidateTag: Invalida el caché basado en etiquetas que asignas a tus peticiones fetch
La segunda opción es más flexible y potente, ya que puedes etiquetar múltiples peticiones con la misma etiqueta y invalidarlas todas de una vez.
El Problema y la Estrategia
Imagina este escenario:
- Tienes un blog con artículos que se publican desde un CMS o backend
- Los artículos se muestran en varias páginas: la página principal, listado de posts, página individual, sitemap, feed RSS
- Quieres cachear todo agresivamente para mejorar el rendimiento (30 días, por ejemplo)
- Pero cuando publicas un nuevo artículo, quieres que aparezca inmediatamente
Sin revalidación, tendrías que esperar 30 días o reducir el tiempo de caché a minutos, lo cual no es ideal.
¿Por qué un Caché de 30 Días?
Puede parecer contradictorio usar un caché tan largo cuando necesitas contenido actualizado. Sin embargo, esta estrategia es correcta y recomendada por varias razones:
Protección de la API: Un caché largo reduce drásticamente la cantidad de peticiones a tu API backend. Esto protege tu servidor de sobrecarga, especialmente en sitios con alto tráfico. Sin caché, cada visita generaría múltiples peticiones a la API.
Rendimiento óptimo: Con un caché de 30 días, Next.js puede servir contenido desde su caché interno sin necesidad de consultar la API en cada solicitud. Esto resulta en tiempos de respuesta extremadamente rápidos.
Revalidación bajo demanda: La clave está en combinar el caché largo con revalidación on-demand. Cuando el contenido cambia, el webhook invalida el caché inmediatamente, forzando a Next.js a obtener datos frescos en la próxima solicitud. Esto proporciona lo mejor de ambos mundos: rendimiento y actualización inmediata.
Costo y escalabilidad: Menos peticiones a la API significa menos costo de infraestructura y mejor escalabilidad. Tu backend puede manejar más tráfico sin necesidad de escalar recursos.
En resumen, el caché de 30 días no es un problema cuando tienes revalidación on-demand. Es una estrategia de optimización que protege tu API mientras garantiza contenido actualizado cuando es necesario.
Implementación
Paso 1: Configurar Tags de Caché en tus Fetches
Primero, necesitas etiquetar todas tus peticiones fetch con tags que puedas referenciar después. Esto es crítico: si una función fetch no tiene tags, no se invalidará cuando el webhook revalide el caché.
Aquí tienes ejemplos de cómo etiquetar tus fetches:
// lib/api.ts
export async function getPosts(): Promise<Article[]> {
const data = await fetchAPI<ApiPostsResponse>('/post', {
next: {
revalidate: 2592000, // 30 días
tags: ['posts', 'posts-list'], // Etiquetas para revalidación
},
})
return data.data.map(mapApiPostToArticle)
}
export const getPostBySlug = cache(async (slug: string): Promise<Article | null> => {
const data = await fetchAPI<ApiPostResponse>(`/post/${slug}`, {
next: {
revalidate: 2592000, // 30 días
tags: ['posts', `post-${slug}`], // Etiqueta general + específica
},
})
return mapApiPostToArticle(data.data)
})
// IMPORTANTE: Todas las funciones de fetch deben estar envueltas con React.cache()
// para deduplicación por request y deben tener tags para poder ser invalidadas
// IMPORTANTE: No olvides agregar tags a TODAS las funciones que usan caché
export async function getAllPostSlugs(): Promise<string[]> {
const data = await fetchAPI<ApiPostsResponse>('/post', {
next: {
revalidate: 2592000,
tags: ['posts', 'posts-list'], // Sin esto, no se invalidará el caché
},
})
return data.data.map(post => post.slug)
}
export const getCategories = cache(async (): Promise<Category[]> => {
const data = await fetchAPI<ApiCategoriesResponse>('/categories', {
next: {
revalidate: 2592000,
tags: ['categories'], // Tags para categorías
},
})
return data.data.map(mapApiCategoryToCategory)
})
Error común: Olvidar agregar tags a funciones como getAllPostSlugs() o getCategories(). Si una función no tiene tags, el webhook no podrá invalidar su caché, y seguirás viendo datos antiguos durante 30 días.
Nota importante: Asegúrate de que tu función fetchAPI acepte correctamente las opciones next. Si estás usando TypeScript, necesitarás extender el tipo:
// lib/api-client.ts
interface NextFetchOptions {
next?: {
revalidate?: number | false
tags?: string[]
}
}
type FetchAPIOptions = RequestInit & NextFetchOptions
export async function fetchAPI<T>(
url: string,
options?: FetchAPIOptions
): Promise<T> {
// ... tu implementación
const response = await fetch(fullUrl, {
...options, // Esto incluye las opciones 'next'
headers: {
'Content-Type': 'application/json',
...options?.headers,
},
})
// ...
}
Paso 2: Crear el Endpoint de Revalidación
Ahora crea un endpoint de API que reciba las notificaciones de tu backend:
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest, NextResponse } from 'next/server'
type ResourceType = 'post' | 'category' | 'page'
type ResourceAction = 'created' | 'updated' | 'deleted' | 'published'
interface ResourceMetadata {
type: ResourceType
id: number
action: ResourceAction
timestamp: number
slug?: string
categorySlug?: string
}
interface RevalidatePayload {
token: string
paths?: string[]
resource?: ResourceMetadata
}
export async function POST(request: NextRequest) {
try {
const body: RevalidatePayload = await request.json()
// 1. Validar el token de seguridad
const expectedToken = process.env.REVALIDATE_TOKEN
if (!expectedToken || body.token !== expectedToken) {
return NextResponse.json(
{ message: 'Invalid token' },
{ status: 401 }
)
}
// 2. Generar tags y paths a revalidar
let tagsToRevalidate: string[] = []
const pathsToRevalidate: string[] = []
if (body.resource) {
const { type, slug, action } = body.resource
// Generar tags según el tipo de recurso
switch (type) {
case 'post':
tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated')
if (slug) {
tagsToRevalidate.push(`post-${slug}`)
}
break
case 'category':
tagsToRevalidate.push('categories') // Revalidar tags de categorías
tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated') // Categorías afectan posts
break
}
// Generar paths según el tipo de recurso
pathsToRevalidate.push('/') // Siempre revalidar home
switch (type) {
case 'post':
if (slug) {
pathsToRevalidate.push(`/post/${slug}`)
}
pathsToRevalidate.push('/post', '/categorias')
if (body.resource.categorySlug) {
pathsToRevalidate.push(`/categorias/${body.resource.categorySlug}`)
}
pathsToRevalidate.push('/sitemap.xml', '/feed.xml')
break
case 'category':
if (slug) {
pathsToRevalidate.push(`/categorias/${slug}`)
}
pathsToRevalidate.push('/categorias', '/post')
break
}
}
// Si no hay resource pero hay paths relacionados con posts, inferir tags
// Esto asegura que el caché se invalide incluso si el backend no envía metadata
if (tagsToRevalidate.length === 0) {
const paths = body.paths || pathsToRevalidate
const hasPostPaths = paths.some(path =>
path === '/post' ||
path.startsWith('/post/') ||
path === '/' ||
path.includes('sitemap') ||
path.includes('feed')
)
const hasCategoryPaths = paths.some(path =>
path === '/categorias' ||
path.startsWith('/categorias/')
)
if (hasPostPaths) {
tagsToRevalidate = ['posts', 'posts-list', 'posts-paginated']
console.log('[Revalidate] No resource provided, inferring post tags from paths')
}
if (hasCategoryPaths) {
if (!tagsToRevalidate.includes('categories')) {
tagsToRevalidate.push('categories')
}
// Las categorías afectan posts
if (!tagsToRevalidate.includes('posts')) {
tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated')
}
}
}
// 3. Revalidar tags (crítico para invalidar el caché de 30 días)
const revalidatedTags: string[] = []
const failedTags: string[] = []
if (tagsToRevalidate.length === 0) {
console.warn('[Revalidate] No cache tags to revalidate - fetch cache may not be invalidated!')
}
for (const tag of tagsToRevalidate) {
try {
// IMPORTANTE: Usar { expire: 0 } para invalidación inmediata
// 'max' solo marca como stale pero no invalida inmediatamente
// expire: 0 fuerza la expiración inmediata para que se obtengan datos frescos en la próxima solicitud
revalidateTag(tag, { expire: 0 })
revalidatedTags.push(tag)
if (process.env.NODE_ENV === 'development') {
console.log(`[Revalidate] Successfully revalidated tag: ${tag}`)
}
} catch (error) {
console.error(`[Revalidate] Error revalidating tag ${tag}:`, error)
failedTags.push(tag)
}
}
// 4. Revalidar paths (si se proporcionaron explícitamente o se generaron)
// IMPORTANTE: Revalidar tanto 'page' como 'layout' para asegurar invalidación completa
const paths = body.paths || pathsToRevalidate
const revalidatedPaths: string[] = []
for (const path of paths) {
try {
// Para rutas dinámicas y estáticas, revalidar tanto page como layout
const isDynamicRoute = path.match(/^\/[^/]+\/[^/]+$/) &&
!path.endsWith('.xml') &&
path !== '/'
if (isDynamicRoute || !path.endsWith('.xml')) {
// Rutas dinámicas y estáticas: revalidar page y layout
revalidatePath(path, 'page')
revalidatePath(path, 'layout')
} else {
// Rutas especiales como /feed.xml
revalidatePath(path)
}
revalidatedPaths.push(path)
} catch (error) {
console.error(`Error revalidating path ${path}:`, error)
}
}
return NextResponse.json({
revalidated: revalidatedPaths.length > 0 || revalidatedTags.length > 0,
paths: revalidatedPaths,
tags: revalidatedTags.length > 0 ? revalidatedTags : undefined,
failedTags: failedTags.length > 0 ? failedTags : undefined,
})
} catch (error) {
console.error('[Revalidate] Error:', error)
return NextResponse.json(
{ message: 'Error revalidating', error: String(error) },
{ status: 500 }
)
}
}
Paso 3: Configurar el Token de Seguridad
Crea una variable de entorno para el token:
REVALIDATE_TOKEN=tu_token_secreto_super_seguro_aqui // .env.local
Importante: Este token debe ser el mismo que uses en tu backend para autenticar las peticiones al endpoint de revalidación.
Cómo Funciona el Flujo Completo
Una vez que tienes configurado el endpoint de revalidación, el flujo es el siguiente:
- Tu backend/CMS dispara un webhook a
/api/revalidatecuando el contenido cambia - El endpoint de Next.js valida el token de seguridad
- Se procesan los datos del recurso y se generan las tags y paths a revalidar
- Se invalidan las tags y paths correspondientes usando
revalidateTag()yrevalidatePath() - En la próxima solicitud, Next.js detecta que el caché fue invalidado y obtiene datos frescos de la API
- Los usuarios ven el contenido actualizado automáticamente sin necesidad de esperar a que expire el tiempo de caché
El formato del payload que espera el endpoint es:
{
"token": "tu_token_secreto",
"resource": {
"type": "post",
"id": 123,
"action": "published",
"timestamp": 1734567890,
"slug": "mi-articulo",
"categorySlug": "laravel"
}
}
También puedes proporcionar paths explícitos si prefieres tener control total:
{
"token": "tu_token_secreto",
"paths": ["/", "/post", "/post/mi-articulo"],
"resource": {
"type": "post",
"id": 123,
"action": "published"
}
}
Mejores Prácticas
1. Usa Tags Específicos y Generales
Combina tags generales con tags específicos:
tags: ['posts', 'posts-list', `post-${slug}`]
Esto te permite:
- Invalidar todos los posts con
revalidateTag('posts') - Invalidar solo un post específico con
revalidateTag('post-mi-articulo')
2. Genera Paths y Tags Automáticamente
En lugar de tener que especificar manualmente todos los paths en cada webhook, genera los paths automáticamente basándote en el tipo de recurso. Además, el webhook puede inferir tags desde los paths si el backend no envía metadata:
function generateRevalidationPaths(resource: ResourceMetadata): string[] {
const paths: string[] = ['/'] // Siempre revalidar home
if (resource.type === 'post' && resource.slug) {
paths.push(`/post/${resource.slug}`)
paths.push('/post', '/categorias')
if (resource.categorySlug) {
paths.push(`/categorias/${resource.categorySlug}`)
}
paths.push('/sitemap-posts.xml', '/feed.xml', '/sitemap.xml')
}
return paths
}
// Si no hay resource, inferir tags desde paths
if (tagsToRevalidate.length === 0) {
const hasPostPaths = paths.some(path =>
path === '/post' || path.startsWith('/post/') || path === '/'
)
if (hasPostPaths) {
tagsToRevalidate = ['posts', 'posts-list', 'posts-paginated']
}
}
Esto asegura que el caché se invalide incluso si el backend no envía el campo resource en el webhook.
3. Maneja Errores Gracefully
El webhook puede fallar por varias razones (red, timeout, etc.). En tu endpoint, asegúrate de manejar errores y devolver respuestas claras:
try {
// ... revalidación ...
} catch (error) {
console.error('[Revalidate] Error:', error)
return NextResponse.json(
{
message: 'Error revalidating',
error: error instanceof Error ? error.message : String(error)
},
{ status: 500 }
)
}
También es buena práctica que tu backend tenga un sistema de reintentos en caso de que el webhook falle.
4. Logging para Debugging
Agrega logging estructurado para poder debuggear problemas. Usa after() de Next.js para hacer el logging de forma asíncrona y no bloquear la respuesta:
import { after } from 'next/server'
// ... después de revalidar ...
after(async () => {
const logData = {
paths: revalidatedPaths,
tags: revalidatedTags.length > 0 ? revalidatedTags : undefined,
failed: failedPaths.length > 0 ? failedPaths : undefined,
failedTags: failedTags.length > 0 ? failedTags : undefined,
resource: resource?.type || undefined,
action: resource?.action || undefined,
}
const logMessage = { event: 'cache_revalidated', ...logData }
if (process.env.NODE_ENV === 'production') {
console.log(JSON.stringify(logMessage))
} else {
console.log('[Revalidate] Cache revalidated:', JSON.stringify(logMessage, null, 2))
}
})
Esto te ayudará a identificar si las tags se están revalidando correctamente y si hay algún problema con el webhook.
Invalidación de Vercel Edge Cache
Vercel Edge Cache es la capa de caché CDN de Vercel que se encuentra delante de tu aplicación Next.js. Es importante entender su relación con el caché de Next.js:
-
Caché de Next.js: Es el caché interno de Next.js que se invalida con
revalidateTag()yrevalidatePath(). Este es el caché principal que controla qué datos se obtienen de tu API. -
Vercel Edge Cache: Es el caché del CDN de Vercel que almacena respuestas completas de páginas. Este caché está delante de Next.js y puede servir contenido sin llegar a tu aplicación.
¿Es necesario invalidar Vercel Edge Cache?
La invalidación de Vercel Edge Cache es opcional pero altamente recomendada:
Sin invalidación de Edge Cache: Next.js seguirá funcionando correctamente. Cuando revalidateTag() invalida el caché de Next.js, la próxima solicitud que llegue a Next.js obtendrá datos frescos. Sin embargo, si Vercel Edge Cache tiene una copia en caché de la página completa, puede seguir sirviendo contenido antiguo desde el CDN sin llegar a Next.js.
Con invalidación de Edge Cache: Garantizas que tanto el caché de Next.js como el caché del CDN se invalidan simultáneamente. Esto asegura que los usuarios siempre vean contenido actualizado, independientemente de si la solicitud se sirve desde el CDN o desde Next.js.
Recomendación: Si tienes acceso a las credenciales de Vercel (VERCEL_TOKEN y VERCEL_PROJECT_ID), es recomendable invalidar también el Edge Cache para una experiencia de usuario óptima. Si no tienes estas credenciales o prefieres simplificar, la aplicación seguirá funcionando correctamente, pero puede haber un pequeño retraso hasta que el Edge Cache expire naturalmente.
Implementación
// Invalidar Vercel Edge Cache
// Esto asegura que el CDN también invalida su caché, no solo Next.js
if (revalidatedTags.length > 0 && process.env.VERCEL_TOKEN && process.env.VERCEL_PROJECT_ID) {
try {
const projectId = process.env.VERCEL_PROJECT_ID
const teamId = process.env.VERCEL_TEAM_ID
const vercelApiUrl = teamId
? `https://api.vercel.com/v1/edge-cache/invalidate-by-tags?projectIdOrName=${projectId}&teamId=${teamId}`
: `https://api.vercel.com/v1/edge-cache/invalidate-by-tags?projectIdOrName=${projectId}`
const response = await fetch(vercelApiUrl, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.VERCEL_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
tags: revalidatedTags,
target: 'production',
}),
})
if (!response.ok) {
const errorText = await response.text()
console.error('[Revalidate] Failed to invalidate Vercel Edge Cache:', errorText)
}
} catch (error) {
// No fallar toda la revalidación si falla la invalidación del edge cache
console.error('[Revalidate] Error invalidating Vercel Edge Cache:', error)
}
}
Variables de entorno necesarias:
VERCEL_TOKEN: Token de API de VercelVERCEL_PROJECT_ID: ID del proyecto en VercelVERCEL_TEAM_ID: (Opcional) ID del equipo si el proyecto pertenece a un equipo
Problemas Comunes y Soluciones
“La revalidación no funciona - el caché de 30 días no se invalida”
Causa 1: Las tags no coinciden exactamente.
Solución: Asegúrate de que las tags que usas en revalidateTag() sean exactamente las mismas que usas en tus fetches. Un espacio extra o una diferencia de mayúsculas hará que no funcione.
Causa 2: Alguna función fetch no tiene tags de caché.
Solución: Revisa que todas tus funciones que usan revalidate: 2592000 tengan tags. Si una función como getAllPostSlugs() o getCategories() no tiene tags, el webhook no podrá invalidar su caché.
Causa 3: El backend no envía el campo resource en el webhook.
Solución: El webhook ahora infiere tags automáticamente desde los paths que se están revalidando. Si revalidas /post o paths relacionados, automáticamente revalidará las tags ['posts', 'posts-list', 'posts-paginated']. Sin embargo, es mejor que el backend siempre envíe el campo resource con el slug para revalidar tags específicas como post-${slug}.
Causa 4: Estás usando revalidateTag(tag, 'max') en lugar de revalidateTag(tag, { expire: 0 }).
Solución: 'max' solo marca el caché como stale pero no lo invalida inmediatamente. Usa { expire: 0 } para forzar la invalidación inmediata del caché.
Causa 5: No estás invalidando Vercel Edge Cache (opcional pero recomendado).
Solución: Next.js revalidateTag solo invalida el caché de Next.js, no el caché de Vercel Edge Cache (CDN). Si bien la invalidación de Edge Cache es opcional (Next.js seguirá funcionando correctamente), es altamente recomendable invalidar también el caché del CDN para garantizar que los usuarios vean contenido actualizado inmediatamente, incluso si la solicitud se sirve desde el CDN. Si tienes acceso a VERCEL_TOKEN y VERCEL_PROJECT_ID, asegúrate de llamar también a la API de Vercel para invalidar el caché del CDN.
“Los componentes del cliente no se actualizan”
Causa: Los componentes del cliente hacen fetch directamente desde el navegador, no desde el servidor.
Solución:
- Los Server Components se actualizarán automáticamente
- Para Client Components, puedes implementar polling o usar
router.refresh()después de detectar cambios
“El webhook tarda mucho en responder”
Causa: Estás haciendo operaciones síncronas pesadas en el endpoint.
Solución: Usa after() de Next.js para hacer el logging de forma asíncrona:
import { after } from 'next/server'
// ... revalidación ...
after(async () => {
// Logging asíncrono que no bloquea la respuesta
console.log('Revalidation completed')
})
Conclusión
La revalidación de caché on-demand es una herramienta poderosa que permite tener lo mejor de ambos mundos: caché agresivo para rendimiento y actualizaciones inmediatas cuando el contenido cambia.
Resumen de la Estrategia
Un caché de 30 días puede parecer excesivo, pero es la estrategia correcta cuando se combina con revalidación on-demand:
- Protege tu API: Reduce drásticamente las peticiones al backend, mejorando la escalabilidad y reduciendo costos.
- Rendimiento óptimo: Next.js puede servir contenido desde caché sin consultar la API en cada solicitud.
- Actualización inmediata: El webhook invalida el caché cuando el contenido cambia, forzando a Next.js a obtener datos frescos en la próxima solicitud.
Esta combinación proporciona rendimiento de caché largo con la frescura de actualización inmediata.
Checklist de Implementación
- Etiqueta todas tus fetches con
tagspara poder referenciarlos después (no olvides ninguna función) - Envuelve tus funciones de fetch con
React.cache()para deduplicación por request - Crea un endpoint
/api/revalidateque reciba webhooks y valide el token - Genera automáticamente las tags y paths a revalidar basándote en el tipo de recurso
- Incluye lógica de inferencia para revalidar tags incluso si el backend no envía metadata completa
- Invalida el caché usando
revalidateTag(tag, { expire: 0 })para invalidación inmediata (no uses'max') - Revalida paths con
revalidatePath(path, 'page')yrevalidatePath(path, 'layout')para invalidación completa - Invalida también Vercel Edge Cache (opcional pero recomendado) usando la API de Vercel si tienes acceso a las credenciales
- Agrega logging estructurado para facilitar el debugging
Puntos Clave a Recordar
- Si una función fetch no tiene tags, el webhook no podrá invalidar su caché. Asegúrate de revisar todas tus funciones que usan
revalidate: 2592000y agregarles tags correspondientes. - Usa
{ expire: 0 }enrevalidateTag, no'max', para invalidación inmediata del caché. - Revalida tanto
'page'como'layout'para asegurar invalidación completa de rutas. - La invalidación de Vercel Edge Cache es opcional pero recomendada para una experiencia óptima.
