Si has trabajado con Next.js y aplicaciones que consumen datos de APIs externas, probablemente te has encontrado con el problema de mantener el contenido actualizado. Tienes contenido que cambia en tu backend (Laravel, Django, o cualquier otra API), 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 el rendimiento. ¿Hay una mejor forma? Sí, y se llama revalidación on-demand o revalidación bajo demanda.
En este artículo te voy a mostrar 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 te 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 que Resolvemos
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.
La Solución: Webhook + Revalidación
La solución es crear un endpoint en Next.js que reciba notificaciones de tu backend cuando el contenido cambia, y que ese endpoint invalide el caché correspondiente.
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:
1// lib/api.ts 2export async function getPosts(): Promise<Article[]> { 3 const data = await fetchAPI<ApiPostsResponse>('/post', { 4 next: { 5 revalidate: 2592000, // 30 días 6 tags: ['posts', 'posts-list'], // Etiquetas para revalidación 7 }, 8 }) 9 10 return data.data.map(mapApiPostToArticle)11}12 13export const getPostBySlug = cache(async (slug: string): Promise<Article | null> => {14 const data = await fetchAPI<ApiPostResponse>(`/post/${slug}`, {15 next: {16 revalidate: 2592000,17 tags: ['posts', `post-${slug}`], // Etiqueta general + específica18 },19 })20 21 return mapApiPostToArticle(data.data)22})23 24// IMPORTANTE: No olvides agregar tags a TODAS las funciones que usan caché25export async function getAllPostSlugs(): Promise<string[]> {26 const data = await fetchAPI<ApiPostsResponse>('/post', {27 next: {28 revalidate: 2592000,29 tags: ['posts', 'posts-list'], // Sin esto, no se invalidará el caché30 },31 })32 33 return data.data.map(post => post.slug)34}35 36export const getCategories = cache(async (): Promise<Category[]> => {37 const data = await fetchAPI<ApiCategoriesResponse>('/categories', {38 next: {39 revalidate: 2592000,40 tags: ['categories'], // Tags para categorías41 },42 })43 44 return data.data.map(mapApiCategoryToCategory)45})
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:
1// lib/api-client.ts 2interface NextFetchOptions { 3 next?: { 4 revalidate?: number | false 5 tags?: string[] 6 } 7} 8 9type FetchAPIOptions = RequestInit & NextFetchOptions10 11export async function fetchAPI<T>(12 url: string,13 options?: FetchAPIOptions14): Promise<T> {15 // ... tu implementación16 const response = await fetch(fullUrl, {17 ...options, // Esto incluye las opciones 'next'18 headers: {19 'Content-Type': 'application/json',20 ...options?.headers,21 },22 })23 // ...24}
Paso 2: Crear el Endpoint de Revalidación
Ahora crea un endpoint de API que reciba las notificaciones de tu backend:
1// app/api/revalidate/route.ts 2import { revalidatePath, revalidateTag } from 'next/cache' 3import { NextRequest, NextResponse } from 'next/server' 4 5type ResourceType = 'post' | 'category' | 'page' 6type ResourceAction = 'created' | 'updated' | 'deleted' | 'published' 7 8interface ResourceMetadata { 9 type: ResourceType 10 id: number 11 action: ResourceAction 12 timestamp: number 13 slug?: string 14 categorySlug?: string 15} 16 17interface RevalidatePayload { 18 token: string 19 paths?: string[] 20 resource?: ResourceMetadata 21} 22 23export async function POST(request: NextRequest) { 24 try { 25 const body: RevalidatePayload = await request.json() 26 27 // 1. Validar el token de seguridad 28 const expectedToken = process.env.REVALIDATE_TOKEN 29 if (!expectedToken || body.token !== expectedToken) { 30 return NextResponse.json( 31 { message: 'Invalid token' }, 32 { status: 401 } 33 ) 34 } 35 36 // 2. Generar tags y paths a revalidar 37 let tagsToRevalidate: string[] = [] 38 const pathsToRevalidate: string[] = [] 39 40 if (body.resource) { 41 const { type, slug, action } = body.resource 42 43 // Generar tags según el tipo de recurso 44 switch (type) { 45 case 'post': 46 tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated') 47 if (slug) { 48 tagsToRevalidate.push(`post-${slug}`) 49 } 50 break 51 case 'category': 52 tagsToRevalidate.push('categories') // Revalidar tags de categorías 53 tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated') // Categorías afectan posts 54 break 55 } 56 } 57 58 // Si no hay resource pero hay paths relacionados con posts, inferir tags 59 // Esto asegura que el caché se invalide incluso si el backend no envía metadata 60 if (tagsToRevalidate.length === 0) { 61 const paths = body.paths || pathsToRevalidate 62 const hasPostPaths = paths.some(path => 63 path === '/post' || 64 path.startsWith('/post/') || 65 path === '/' || 66 path.includes('sitemap') || 67 path.includes('feed') 68 ) 69 70 const hasCategoryPaths = paths.some(path => 71 path === '/categorias' || 72 path.startsWith('/categorias/') 73 ) 74 75 if (hasPostPaths) { 76 tagsToRevalidate = ['posts', 'posts-list', 'posts-paginated'] 77 console.log('[Revalidate] No resource provided, inferring post tags from paths') 78 } 79 80 if (hasCategoryPaths) { 81 if (!tagsToRevalidate.includes('categories')) { 82 tagsToRevalidate.push('categories') 83 } 84 // Las categorías afectan posts 85 if (!tagsToRevalidate.includes('posts')) { 86 tagsToRevalidate.push('posts', 'posts-list', 'posts-paginated') 87 } 88 } 89 } 90 91 // Generar paths según el tipo de recurso 92 pathsToRevalidate.push('/') // Siempre revalidar home 93 94 switch (type) { 95 case 'post': 96 if (slug) { 97 pathsToRevalidate.push(`/post/${slug}`) 98 } 99 pathsToRevalidate.push('/post', '/categorias')100 if (body.resource.categorySlug) {101 pathsToRevalidate.push(`/categorias/${body.resource.categorySlug}`)102 }103 pathsToRevalidate.push('/sitemap.xml', '/feed.xml')104 break105 case 'category':106 if (slug) {107 pathsToRevalidate.push(`/categorias/${slug}`)108 }109 pathsToRevalidate.push('/categorias', '/post')110 break111 }112 }113 114 // 3. Revalidar tags (crítico para invalidar el caché de 30 días)115 const revalidatedTags: string[] = []116 const failedTags: string[] = []117 118 if (tagsToRevalidate.length === 0) {119 console.warn('[Revalidate] No cache tags to revalidate - fetch cache may not be invalidated!')120 }121 122 for (const tag of tagsToRevalidate) {123 try {124 // Usar 'max' para asegurar que el caché de 30 días se invalide correctamente125 revalidateTag(tag, 'max')126 revalidatedTags.push(tag)127 if (process.env.NODE_ENV === 'development') {128 console.log(`[Revalidate] Successfully revalidated tag: ${tag}`)129 }130 } catch (error) {131 console.error(`[Revalidate] Error revalidating tag ${tag}:`, error)132 failedTags.push(tag)133 }134 }135 136 // 4. Revalidar paths (si se proporcionaron explícitamente o se generaron)137 const paths = body.paths || pathsToRevalidate138 const revalidatedPaths: string[] = []139 for (const path of paths) {140 try {141 revalidatePath(path)142 revalidatedPaths.push(path)143 } catch (error) {144 console.error(`Error revalidating path ${path}:`, error)145 }146 }147 148 return NextResponse.json({149 revalidated: revalidatedPaths.length > 0 || revalidatedTags.length > 0,150 paths: revalidatedPaths,151 tags: revalidatedTags.length > 0 ? revalidatedTags : undefined,152 failedTags: failedTags.length > 0 ? failedTags : undefined,153 })154 } catch (error) {155 console.error('[Revalidate] Error:', error)156 return NextResponse.json(157 { message: 'Error revalidating', error: String(error) },158 { status: 500 }159 )160 }161}
Paso 3: Configurar el Token de Seguridad
Crea una variable de entorno para el token:
1# .env.local2REVALIDATE_TOKEN=tu_token_secreto_super_seguro_aqui
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 cambiaEl 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:
1{ 2 "token": "tu_token_secreto", 3 "resource": { 4 "type": "post", 5 "id": 123, 6 "action": "published", 7 "timestamp": 1734567890, 8 "slug": "mi-articulo", 9 "categorySlug": "laravel"10 }11}
También puedes proporcionar paths explícitos si prefieres tener control total:
1{2 "token": "tu_token_secreto",3 "paths": ["/", "/post", "/post/mi-articulo"],4 "resource": {5 "type": "post",6 "id": 123,7 "action": "published"8 }9}
Mejores Prácticas
1. Usa Tags Específicos y Generales
Combina tags generales con tags específicos:
1tags: ['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:
1function generateRevalidationPaths(resource: ResourceMetadata): string[] { 2 const paths: string[] = ['/'] // Siempre revalidar home 3 4 if (resource.type === 'post' && resource.slug) { 5 paths.push(`/post/${resource.slug}`) 6 paths.push('/post', '/categorias') 7 if (resource.categorySlug) { 8 paths.push(`/categorias/${resource.categorySlug}`) 9 }10 paths.push('/sitemap-posts.xml', '/feed.xml', '/sitemap.xml')11 }12 13 return paths14}15 16// Si no hay resource, inferir tags desde paths17if (tagsToRevalidate.length === 0) {18 const hasPostPaths = paths.some(path =>19 path === '/post' || path.startsWith('/post/') || path === '/'20 )21 22 if (hasPostPaths) {23 tagsToRevalidate = ['posts', 'posts-list', 'posts-paginated']24 }25}
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:
1try { 2 // ... revalidación ... 3} catch (error) { 4 console.error('[Revalidate] Error:', error) 5 return NextResponse.json( 6 { 7 message: 'Error revalidating', 8 error: error instanceof Error ? error.message : String(error) 9 },10 { status: 500 }11 )12}
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:
1import { after } from 'next/server' 2 3// ... después de revalidar ... 4 5after(async () => { 6 const logData = { 7 paths: revalidatedPaths, 8 tags: revalidatedTags.length > 0 ? revalidatedTags : undefined, 9 failed: failedPaths.length > 0 ? failedPaths : undefined,10 failedTags: failedTags.length > 0 ? failedTags : undefined,11 resource: resource?.type || undefined,12 action: resource?.action || undefined,13 }14 15 const logMessage = { event: 'cache_revalidated', ...logData }16 if (process.env.NODE_ENV === 'production') {17 console.log(JSON.stringify(logMessage))18 } else {19 console.log('[Revalidate] Cache revalidated:', JSON.stringify(logMessage, null, 2))20 }21})
Esto te ayudará a identificar si las tags se están revalidando correctamente y si hay algún problema con el webhook.
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}.
"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:
1import { after } from 'next/server'2 3// ... revalidación ...4 5after(async () => {6 // Logging asíncrono que no bloquea la respuesta7 console.log('Revalidation completed')8})
Conclusión
La revalidación de caché on-demand es una herramienta poderosa que te permite tener lo mejor de ambos mundos: caché agresivo para rendimiento y actualizaciones inmediatas cuando el contenido cambia.
La implementación desde el lado de Next.js es relativamente sencilla:
Etiqueta TODAS tus fetches con
tagspara poder referenciarlos después (no olvides ninguna función)Crea un endpoint
/api/revalidateque reciba webhooks y valide el tokenGenera 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, 'max')yrevalidatePath()Agrega logging estructurado para facilitar el debugging
¡Disfruta de contenido siempre actualizado sin sacrificar rendimiento!
Recordatorio importante: 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: 2592000 y agregarles tags correspondientes.
¿Has implementado revalidación en tus proyectos? ¿Qué desafíos encontraste? Déjame saber en los comentarios.
Recursos adicionales:
