Implementando ISR em Nuxt 3 (Nitro): gerar, invalidar e cache por rota
Por John Pires · 13 de março de 2026

Implementando ISR em Nuxt 3 (Nitro): gerar, invalidar e cache por rota
Contexto e motivacao
Por que ISR em Nuxt 3?
Incremental Static Regeneration (ISR) permite servir conteúdo estático com a capacidade de atualizá-lo sob demanda ou periodicamente sem precisar rebuildar toda a aplicação. Em Nuxt 3, que roda sobre Nitro, não existe um método "oficial" idêntico ao do Next.js, mas você pode implementar um circuito de ISR controlado usando server routes, cache persistente e background revalidation.
Quando usar ISR em Nuxt 3
Use ISR quando você tem páginas que são majoritariamente estáticas, mas que precisam refletir atualizações do conteúdo com latência controlada — por exemplo, posts de blog, páginas de produto ou páginas de marketing. ISR reduz carga de origem e tempo de build enquanto mantém conteúdo relativamente fresco.
Este artigo foca em um padrão prático: cache por rota de dados (JSON), revalidação em background (stale-while-revalidate) e endpoint seguro de invalidação. A escolha por cache de API torna o approach compatível com SSR e SSG no Nuxt, mantendo renderização server-side simples.
Como funciona por baixo dos panos
Arquitetura resumida
Resumo do fluxo que vamos implementar:
- Rota API /api/posts/[slug] retorna JSON de conteúdo.
- A rota mantém um cache em disco (ou /tmp) com metadata de expiração.
- Se o cache estiver válido, retorna imediatamente.
- Se expirado, retorna o conteúdo stale imediatamente, dispara uma regeneração em background e atualiza o cache.
- Há um endpoint /api/revalidate para invalidação manual com secret.
Decisões técnicas e trade-offs
Optamos por cache de dados e não HTML para simplicidade: renderizar HTML programaticamente fora do pipeline Nuxt pode complicar contexto SSR, hooks e plugins. Cache de JSON é suficiente para a maioria dos casos e reduz pontos de falha.
Armazenamento em disco é simples e portátil. Para escala, substitua por Redis, S3 ou outro storage que exponha get/set com TTL. O padrão do código é genérico: o adaptador de armazenamento pode ser trocado sem alterar consumer da rota.
Implementacao passo a passo
1) Estrutura de arquivos
Crie estas rotas dentro da pasta /server/api do seu projeto Nuxt 3:
- /server/api/posts/[slug].ts — API ISR por rota
- /server/api/revalidate.ts — endpoint para invalidar/revalidar cache
- /composables/usePost.ts — composable cliente para consumir a API
2) Endpoint ISR: /server/api/posts/[slug].ts
O handler abaixo implementa o padrão stale-while-revalidate com cache em disco. Antes do bloco de código eu explico o que faz: ele tenta ler um arquivo JSON de cache com shape { meta: { expiresAt }, data }. Se válido, retorna. Se expirado, retorna conteúdo stale e dispara regeneration async.
import { promises as fs } from 'fs'
import { resolve } from 'path'
import { H3Event } from 'h3'
const CACHE_DIR = process.env.POST_CACHE_DIR || './.nitro/post-cache'
const TTL_MS = 1000 * 60 * 5 // 5 minutos de validade
async function fetchFromOrigin(slug: string) {
// Substitua esta função por fetch real de CMS/DB
const res = await fetch(`https://example.com/content/${slug}`)
if (!res.ok) throw new Error('origin fetch failed')
return res.json()
}
export default defineEventHandler(async (event: H3Event) => {
const slug = event.context.params?.slug
n if (!slug) return sendError(event, createError({ statusCode: 400, statusMessage: 'missing slug' }))
const filePath = resolve(CACHE_DIR, `${slug}.json`)
// try read cache
try {
const raw = await fs.readFile(filePath, 'utf-8')
const parsed = JSON.parse(raw)
const expiresAt = parsed.meta?.expiresAt || 0
if (Date.now() < expiresAt) {
// cache valido
return { source: 'cache', data: parsed.data }
}
// cache expirado -> revalidar em background e retornar stale
revalidateInBackground(slug, filePath).catch(err => console.error('reval err', err))
return { source: 'stale-cache', data: parsed.data }
} catch (err: any) {
// cache nao existe ou leitura falhou -> gerar synchronously
try {
const data = await fetchFromOrigin(slug)
const meta = { expiresAt: Date.now() + TTL_MS }
await fs.mkdir(CACHE_DIR, { recursive: true })
await fs.writeFile(filePath, JSON.stringify({ meta, data }), 'utf-8')
return { source: 'origin', data }
} catch (e) {
return sendError(event, createError({ statusCode: 502, statusMessage: 'failed to fetch origin' }))
}
}
})
async function revalidateInBackground(slug: string, filePath: string) {
const data = await fetchFromOrigin(slug)
const meta = { expiresAt: Date.now() + TTL_MS }
await fs.writeFile(filePath, JSON.stringify({ meta, data }), 'utf-8')
}
O que acontece depois: a primeira requisição sem cache faz fetch síncrono e popula o cache. Depois da expiração, a rota devolve o stale rapidamente e inicia um fetch em background que atualiza o cache para próximas requisições.
3) Endpoint de revalidação manual
Um endpoint simples para invalidar o cache e, opcionalmente, forçar regeneração. Use uma chave secreta para limitar acesso.
import { promises as fs } from 'fs'
import { resolve } from 'path'
const CACHE_DIR = process.env.POST_CACHE_DIR || './.nitro/post-cache'
const REVALIDATE_SECRET = process.env.REVALIDATE_SECRET || 'changeme'
export default defineEventHandler(async (event) => {
if (event.node.req.method !== 'POST') return sendError(event, createError({ statusCode: 405 }))
const body = await readBody(event)
if (body?.secret !== REVALIDATE_SECRET) return sendError(event, createError({ statusCode: 401 }))
const slug = body.slug
if (!slug) return sendError(event, createError({ statusCode: 400, statusMessage: 'slug required' }))
const filePath = resolve(CACHE_DIR, `${slug}.json`)
try {
await fs.unlink(filePath)
} catch (e) {
// ignore if not exists
}
// opcional: disparar reval imediata (fire-and-forget)
event.node.res.statusCode = 200
return { revalidated: true, slug }
})
Depois de invalidar, a próxima chamada ao endpoint de posts fará fetch origin e repovoará o cache.
4) Consumindo a API do lado do servidor na página
Consumo dentro de pages/posts/[slug].vue usando useAsyncData no server side para manter SSR e fallback de SEO.
<script setup lang="ts">
import { useRoute } from 'vue-router'
const route = useRoute()
const slug = route.params.slug as string
const { data: post, pending, error } = await useAsyncData(
`post-${slug}`,
() => $fetch(`/api/posts/${slug}`)
)
</script>
<template>
<div v-if="error">Erro: {{ error.message }}</div>
<article v-else>
<h1>{{ post.data.title }}</h1>
<div v-html="post.data.body" />
</article>
</template>
O que acontece: a página é SSR com os dados retornados pelo endpoint. Se o cache estiver stale, o HTML ainda terá o conteúdo antigo, enquanto o cache é refrescado em background.
Patterns e boas praticas
Segurança e invalidação
Use sempre um secret para endpoints de invalidação. Regeneração automática é útil, mas a invalidação manual é necessária para integrações com CMS webhooks.
Recomendo que o webhook do CMS invoque /api/revalidate com o slug alterado e o secret. Assim as mudanças aparecem quase imediatamente sem expor endpoints públicos.
TTL, stale-while-revalidate e SLAs
Escolha o TTL conforme a tolerância a staleness do seu produto. Para notícias, pode ser 30s; para marketing, 5-10 minutos. Combine TTL com monitoramento de tempo de revalidação para garantir SLAs.
Se revalidação falhar repetidamente, exponha métricas (Prometheus/Grafana) ou logs que permitam identificar problemas no fetch da origem.
Swappable storage (adapter pattern)
O exemplo usa disco. Extraia um adaptador simples com métodos get/set/delete para trocar por Redis ou S3. Exemplo de interface:
interface CacheAdapter {
get(key: string): Promise<{meta: {expiresAt:number}, data:any}|null>
set(key: string, payload: any, ttlMs: number): Promise<void>
del(key: string): Promise<void>
}
Ao isolar essa camada, migrar para infra gerenciada é trivial.
Armadilhas comuns e como evitar
1) Não bloquear request enquanto revalida
Erros comuns: revalidar síncronamente no request path — isso causa latência alta na primeira requisição pós-expiração. Use fire-and-forget ou filas.
Tip: no Node.js você pode disparar promise sem await. Em ambientes serverless, garanta que a execução em background não seja cortada — prefira filas/worker ou invalidação seguida de rebuild por worker.
2) Corrida de escrita no cache
Se muitas requisições chegam simultâneas para a mesma rota sem cache, múltiplas regenerações podem ocorrer. Estratégias para evitar:
- Locking leve: write-ahead file lock ou lock em Redis (SETNX).
- Deduplicação: checar e esperar um curto período antes de disparar nova fetch.
Exemplo rápido de dedupe (pseudo): ao perceber cache ausente, tente criar um arquivo .lock; desaparecido, aguarde até 200ms para ver se outro processo já criou cache.
3) Falha de revalidação contínua
Se a fetch para origem falhar, não sobrescreva o cache com erro. Mantenha o stale e registre incidente. Circuit breaker pode bloquear novas tentativas por um período crescente.
4) Tamanho do cache e armazenamento
Caches em disco podem crescer indefinidamente. Adicione políticas de expurgo e tamanho máximo. Para Redis, use TTL nativo. Para S3, considere Lifecycle rules.
Também controlamos o conteúdo a cachear: guarde apenas o necessário (JSON minimal), evite armazenar binários ou imagens — mantenha imagens em CDN.
Armadilhas de infraestrutura e monitoramento
Observabilidade
Registre métricas básicas: cache hit ratio, reval success/fail, latency da origem. Sem isso, será difícil entender o comportamento em produção.
Erros de permissões e paths
Se usar disco, em ambientes serverless (Vercel, Netlify) o sistema de arquivos pode não ser persistente. Para esses provedores, troque para Redis ou um bucket. Em containers, monte volume persistente.
Teste local e CI para garantir permissão de escrita e criação de diretórios antes de lançar.
Conclusao pratica
Implementar ISR em Nuxt 3 com Nitro é uma questão de padrão: cache de API por rota, revalidação em background e endpoints de invalidação seguros. O exemplo mostrado é operacional, portável e fácil de estender para Redis/S3 quando necessário.
Checklist rápido para levar a produção:
- Extrair adaptador de storage e trocar para Redis/S3 conforme ambiente.
- Adicionar locking/deduplicação para evitar thundering herd.
- Expor endpoint seguro de revalidate e integrar com webhooks do CMS.
- Adicionar métricas e alertas para falhas na revalidação.
Com esses passos você terá um ISR seguro e previsível em Nuxt 3, mantendo builds rápidos e conteúdo fresco sem sacrificar performance.
Próximo passo prático: extraia a lógica de cache para um composable/server util e implemente um adapter Redis para ambientes serverless. Comece substituindo fs por um adapter que respeite a mesma interface e valide localmente.
Se quiser, posso gerar uma versão do adaptador para Redis + exemplo de bloqueio SETNX e integração com webhook do Contentful/Ghost/Strapi.
Assuntos: Nuxt.js, nitro, isr, caching, server-routes
Leia também
Nuxt: criar um módulo que auto-importa composables e server routes com cache SSR
Módulo Nuxt que automatiza auto-import de composables e registro de server routes, adicionando um wrapper de cache SSR configurável. Reduz boilerplate…
Scroll infinito reativo no Vue 3 com Composition API e Pinia
Aprenda a implementar scroll infinito reativo em Vue 3 usando Composition API e Pinia: do setup ao mini-projeto, com exemplos práticos, store…
Sincronizando estado entre abas com Pinia e BroadcastChannel (Composition API)
Aprenda a sincronizar estado entre abas usando Pinia e BroadcastChannel com Composition API. Guia passo a passo para iniciantes: setup, exemplos funcionais…
Streaming SSR em Nuxt 3: server routes Nitro com SSE e NDJSON e consumo no cliente
Como implementar streaming eficiente em Nuxt 3 usando Nitro: compare o anti-padrão de acumular tudo com as rotas SSE/NDJSON corretas, composables cliente…
Autenticação SSR em Nuxt 3 com refresh tokens rotativos e server routes
Guia prático para implementar autenticação SSR em Nuxt 3 usando refresh tokens rotativos, cookies httpOnly e server routes. Padrões, code samples e armadilhas…
useServerQuery no Nuxt 3: composable SSR que deduplica, cache e revalida
Composble server-only para Nuxt 3: deduplicação de requisições, cache por processo e stale-while-revalidate prático. Código real, testes e armadilhas para…