Cache tags en Next.js: invalida varias rutas con una línea
Un solo tag compartido entre funciones distintas puede invalidar el catálogo completo y cada página de producto con una sola llamada. Así se diseña esa relación.
Tienes un catálogo con una página de listado y una página por cada producto individual. Alguien compra el último producto en stock. ¿Cuántas llamadas de invalidación necesitas para que ambas rutas reflejen el cambio?
Si tu respuesta fue “dos, una por cada ruta”, hay una forma más limpia — y es justo el punto que mucha gente se salta cuando recién empieza a usar tags en Next.js: el tag no pertenece a una ruta, pertenece a un dato. Si diseñas bien tus tags desde el inicio, una sola invalidación cubre todo lo que depende de ese dato, sin importar en cuántas rutas distintas aparezca.
Este artículo asume que ya conoces la diferencia entre revalidateTag y updateTag — si no, te recomiendo primero leer la matriz de decisión completa.
El error de diseño más común: un tag por ruta
Es tentador pensar en tags como “el nombre de la página que quiero invalidar”:
// ❌ Enfoque ingenuo: un tag por ruta
export const getProducts = unstable_cache(
async () => prisma.product.findMany(),
['products-list'],
{ tags: ['home-page'] } // atado a UNA ruta específica
)
export const getProductBySlug = unstable_cache(
async (slug: string) => prisma.product.findUnique({ where: { slug } }),
['product-by-slug'],
{ tags: ['product-detail-page'] } // otro tag, otra ruta
)
Con este diseño, cuando el stock de un producto cambia, necesitas invalidar dos tags distintos para cubrir el listado y el detalle — y si mañana agregas una tercera ruta que también muestra productos (una página de “ofertas”, por ejemplo), necesitas acordarte de añadir un tercer updateTag() en cada lugar del código donde se actualiza stock. Es frágil: cada nueva ruta que consume el mismo dato exige tocar la lógica de invalidación en todos los puntos de mutación.
El enfoque correcto: el tag describe el dato, no la ruta
// ✅ El tag describe QUÉ dato es, no dónde se muestra
export const getProducts = unstable_cache(
async () => {
console.log('🔴 QUERY: getProducts')
return prisma.product.findMany({ orderBy: { createdAt: 'desc' } })
},
['products-list'],
{ tags: ['products'], revalidate: 3600 }
)
export const getProductBySlug = unstable_cache(
async (slug: string) => {
console.log(`🔴 QUERY: getProductBySlug(${slug})`)
return prisma.product.findUnique({ where: { slug } })
},
['product-by-slug'],
{ tags: ['products'], revalidate: 3600 }
)
Ambas funciones comparten el tag 'products', aunque alimenten rutas completamente distintas (/ y /producto/[slug]). Ahora, la Server Action que descuenta stock solo necesita una línea:
'use server'
import { updateTag } from 'next/cache'
export async function submitCheckout(/* ...datos del pedido... */) {
await prisma.$transaction(async (tx) => {
// crear orden, descontar stock de cada producto comprado
})
// Una sola línea invalida TODO lo etiquetado 'products':
// el listado en home, y cada página de producto individual
updateTag('products')
}
Cuando mañana agregues una página de “ofertas” que también usa getProducts() con este mismo tag, la invalidación ya la cubre automáticamente — no necesitas tocar ni una línea de la Server Action.
Por qué esto no es solo “más limpio”, es más correcto
La diferencia no es solo estética. Un tag por ruta es un acoplamiento oculto: tu lógica de negocio (¿qué pasó? “se vendió stock”) termina sabiendo demasiado sobre detalles de presentación (¿en qué páginas se muestra ese dato?). Eso es una violación silenciosa de separación de responsabilidades — y es exactamente el tipo de acoplamiento que hace que agregar una feature nueva se sienta más riesgoso de lo que debería.
Con tags basados en el dato, la regla mental se simplifica a una sola pregunta al momento de escribir cualquier función de acceso a datos: “¿qué otras funciones muestran esencialmente el mismo dato que yo?” — todas esas comparten el tag.
Un caso con más de un tag: cuando un dato depende de otro
No todo es un tag único. Piensa en la página de un producto individual, que a veces también necesita invalidarse por razones específicas de ese producto (por ejemplo, si solo cambia la descripción de un producto puntual, no quieres invalidar el listado completo).
export const getProductBySlug = unstable_cache(
async (slug: string) => {
return prisma.product.findUnique({ where: { slug } })
},
['product-by-slug'],
{
tags: ['products', `product-${slug}`], // tag general + tag específico
revalidate: 3600,
}
)
Ahora tienes dos niveles de invalidación disponibles según el alcance del cambio:
updateTag('products') // invalida TODO el catálogo (ej: cambió el stock global)
updateTag(`product-${productId}`) // invalida SOLO ese producto (ej: el admin editó su descripción)
Esta granularidad evita invalidaciones innecesariamente amplias — no tiene sentido regenerar el listado completo de la home solo porque alguien corrigió un typo en la descripción de un solo producto.
Reglas prácticas para diseñar tus tags
- Piensa en el dato, no en la ruta. Antes de escribir
tags: [...], pregúntate qué otras funciones de tu código consultan esencialmente lo mismo. - Usa tags jerárquicos cuando aplique. Un tag amplio (
'products') más uno específico (`product-${id}`) te da control fino sin sacrificar el atajo del tag general. - La invalidación vive junto a la mutación, no junto a la ruta. El lugar correcto para decidir qué tag invalidar es la Server Action que cambia el dato — no algo que configuras “por si acaso” en cada página.
- Evita tags demasiado genéricos. Un tag como
'data'que cubre absolutamente todo termina invalidando cosas que no necesitabas tocar, perdiendo el beneficio del cache selectivo.
Conclusión
Diseñar bien tus cache tags es, en el fondo, un ejercicio de modelado de datos, no de configuración de Next.js. Cuando el tag representa el dato real y no la ruta donde se muestra, agregar una nueva vista de ese mismo dato no te obliga a tocar tu lógica de invalidación — simplemente funciona.
¿Tu equipo está diseñando la arquitectura de cache de una aplicación Next.js desde cero? Conversemos 15 minutos sin costo sobre cómo estructurarla bien desde el principio.
¿Tienes un proyecto en mente?
Convierte tu idea en un producto real
Desarrollo web, aplicaciones a medida y consultoría tecnológica para empresas y startups. Cuéntame tu proyecto y te respondo en menos de 24 horas.