Destino Dev

Blog

ISR on-demand com Server Actions: revalidação via webhook e middleware

Por John Pires · 08 de março de 2026

ISR on-demand com Server Actions: revalidação via webhook e middleware

1. Problema real que resolve

Contexto operacional

Equipes que usam Next.js App Router com páginas renderizadas no servidor (SSR) frequentemente combinam essa abordagem com ISR (Incremental Static Regeneration) para reduzir latência e custo. Cenários típicos: um CMS (Contentful, Sanity, Strapi) atualiza conteúdo, uma pipeline faz deploy de assets, ou um usuário edita um post. O objetivo é ter conteúdo atualizado na CDN sem rebuild completo.

Por que revalidação on-demand é necessária

Revalidação on-demand permite que você atualize apenas rotas afetadas quando um evento externo acontece. Isso evita rebuilds caros e reduz o tempo entre publicação e disponibilidade do conteúdo em produção. Em conjunto com Server Actions, dá para acionar revalidações de forma controlada a partir do frontend ou de webhooks externos.

Problemas reais que aparecem em times pequenos: vazamento de segredos, endpoint de revalidate público, chamadas desnecessárias que invalidam cache em massa, e lógica de autorização fraca. Este artigo foca em como evoluir de padrões inseguros/ineficientes para uma arquitetura que usa Server Actions, webhooks verificados e middleware com validações.

2. Abordagem ingênua vs abordagem otimizada

Jeito errado (ingênuo)

O padrão mais comum que vejo em code reviews é: criar uma rota /api/revalidate que usa um secret em query string e chamar essa rota diretamente do cliente sempre que um conteúdo muda. Ou pior: expor o secret no bundle do frontend. Isso cria dois riscos imediatos: (1) secret pode vazar e qualquer um consegue revalidar rotas; (2) revalidações em massa por clientes ou bots podem causar cache-thrashing.

Exemplo de problemas práticos: um cliente faz uma chamada após cada digitação em um formulário, ou scripts automatizados convocam o endpoint em loop. Além disso, sem verificação de origem, é trivial invocar a rota a partir de qualquer lugar.

Jeito certo (otimizado)

Boa prática: mover a lógica de revalidação para o servidor seguro usando Server Actions ou route handlers dedicados; aceitar apenas webhooks assinados; usar middleware para filtrar tráfego e aplicar rate-limiting básico; e disparar revalidação por tag/path granular com revalidatePath/revalidateTag. Isso reduz risco e melhora eficiência — apenas as rotas afetadas são revalidadas.

Além disso, Server Actions permitem que componentes cliente invoquem lógica segura sem expor segredos: a chamada acontece do cliente para o servidor (via RPC implícita do Next.js), e o servidor executa revalidatePath. Combine com checagens de permissões (autorização baseada em session/cookies) para controles finos.

Dica: Prefira revalidateTag quando seu conteúdo é indexado por tags; use revalidatePath para rotas específicas. RevalidateTag é excelente para páginas listadas por categoria.

3. Implementação com código real

3.1. Exemplo 1 — Jeito errado (vulnerável)

O que faz: rota API simples que aceita um secret na query e chama revalidatePath. O segredo muitas vezes acaba no bundle do cliente.

// app/api/revalidate/route.ts (JEITO ERRADO — NÃO USE em produção)
import { NextResponse } from 'next/server'

export async function POST(req: Request) {
  const { searchParams } = new URL(req.url)
  const secret = searchParams.get('secret')
  const path = searchParams.get('path')

  // secret hardcoded ou passado pelo cliente
  if (secret !== process.env.REVALIDATE_SECRET) {
    return NextResponse.json({ ok: false }, { status: 401 })
  }

  try {
    // Pages router/res.revalidate não disponível — exemplo conceitual
    // Em App Router use revalidatePath
    // Esta implementação aceita qualquer path e pode ser abusada
    await revalidatePath(path || '/')
    return NextResponse.json({ ok: true })
  } catch (err) {
    return NextResponse.json({ ok: false, error: String(err) }, { status: 500 })
  }
}

O que acontece: qualquer cliente que saiba o secret consegue revalidar rotas. Alguns times colocam o secret em variáveis JS injetadas no cliente, expondo-o sem querer.

3.2. Exemplo 2 — webhook seguro com HMAC (route handler)

O que faz: cria um route handler em app/api/webhook/route.ts que valida um header HMAC, decodifica payload e chama revalidatePath apenas para rotas relevantes.

// app/api/webhook/route.ts
import { NextResponse } from 'next/server'
import { revalidatePath } from 'next/cache'
import crypto from 'crypto'

const SECRET = process.env.WEBHOOK_SECRET || ''

function verifySignature(body: string, signature: string | null) {
  if (!signature) return false
  const hmac = crypto.createHmac('sha256', SECRET)
  hmac.update(body)
  const digest = `sha256=${hmac.digest('hex')}`
  return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(signature))
}

export async function POST(req: Request) {
  const signature = req.headers.get('x-hub-signature-256')
  const body = await req.text()

  if (!verifySignature(body, signature)) {
    return new NextResponse('invalid signature', { status: 401 })
  }

  const payload = JSON.parse(body)
  // exemplo: payload = { type: 'post.updated', slug: 'hello-world' }
  if (payload.type === 'post.updated' && payload.slug) {
    await revalidatePath(`/posts/${payload.slug}`)
    return NextResponse.json({ revalidated: true })
  }

  return NextResponse.json({ ignored: true })
}

O que acontece: apenas webhooks com HMAC correto conseguem disparar revalidação. O handler chama revalidatePath de forma granular — reduzindo carga e evitando revalidação desnecessária.

3.3. Exemplo 3 — Server Action para revalidar a partir do frontend

O que faz: uma Server Action que só usuários autenticados podem invocar para revalidar uma rota. Útil quando editores no painel querem atualizar cache após publicar.

// app/actions/revalidate.ts
'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'

export async function revalidatePost(slug: string) {
  // checar sessão via cookie (exemplo simples)
  const session = cookies().get('session')?.value
  if (!session || session !== process.env.EDITOR_SESSION_TOKEN) {
    throw new Error('unauthorized')
  }

  // revalida apenas a rota necessária
  await revalidatePath(`/posts/${slug}`)
  return { ok: true }
}

O que acontece: o Server Action roda no servidor. O cliente importa essa função e a chama diretamente; o segredo de verificação (EDITOR_SESSION_TOKEN) nunca vai pro cliente. Isso evita expor APIs públicas.

4. Testes e validação

4.1. Testes locais com curl e HMAC

Para validar o webhook localmente, gere a assinatura e envie um POST. Isso simula um provedor externo (CMS) que assina payloads.

// send-webhook.js — script node para testar
const crypto = require('crypto')
const fetch = require('node-fetch')

const SECRET = process.env.WEBHOOK_SECRET || 'secret'
const payload = JSON.stringify({ type: 'post.updated', slug: 'hello-world' })
const hmac = crypto.createHmac('sha256', SECRET)
hmac.update(payload)
const signature = `sha256=${hmac.digest('hex')}`

fetch('http://localhost:3000/api/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json', 'x-hub-signature-256': signature },
  body: payload,
}).then(r => r.text()).then(console.log)

O que acontece: se a implementação aceitar a assinatura, você verá resposta com revalidated: true. Caso contrário, receberá 401. Esse teste é essencial antes de conectar seu CMS à rota.

4.2. Unit tests e mocks para Server Actions

Server Actions podem ser testadas isoladamente. Mocke revalidatePath e as headers/cookies para verificar comportamento em falha e sucesso. Exemplo rápido com jest:

// revalidate.test.ts
import { revalidatePost } from './app/actions/revalidate'
import * as cache from 'next/cache'

jest.mock('next/cache', () => ({ revalidatePath: jest.fn() }))

test('revalidatePost unauthorized throws', async () => {
  // simular ausência de cookie: cookies() precisa ser mockado
  // para foco mostramos apenas o fluxo de autorização
  process.env.EDITOR_SESSION_TOKEN = 'token123'
  // set cookies mock to not return session
  await expect(revalidatePost('hello')).rejects.toThrow('unauthorized')
})

test('revalidatePost calls revalidatePath', async () => {
  process.env.EDITOR_SESSION_TOKEN = 'token123'
  // aqui você deve mockar cookies() para retornar session
  // e então:
  // await revalidatePost('hello')
  // expect(cache.revalidatePath).toHaveBeenCalledWith('/posts/hello')
})

O que acontece: você valida a lógica de autorização e a chamada para revalidatePath sem tocar na infraestrutura. Para integração, rode testes end-to-end contra um ambiente staging.

5. Considerações de produção

Segurança: segredos, HMAC e autorização

Não exponha segredos no cliente. Use HMAC para webhooks vindos de terceiros. Valide assinaturas com timing-safe comparisons. Para endpoints internos (painel administrativo), prefira Server Actions com checagem de sessão/cookie e autorização baseada em roles.

Adote rotação de segredos e registre chamadas falhas em logs para detectar tentativas de abuso. Mantenha limites no tamanho do payload e valide o conteúdo JSON antes de parsear para evitar DoS via memória.

Operação: rate limiting, observability e retries

Implemente rate-limiting na borda (Vercel Edge Middleware, Cloudflare Workers ou CDN rules) para evitar flood. Logue eventos de revalidação com metadata (route, origin, timestamp). Use retries exponenciais no remetente do webhook, mas no receptor seja idempotente — a revalidação é uma operação que pode acontecer várias vezes sem efeito adverso.

Middleware: exemplo e papel

Middleware é útil para filtrar tráfego e aplicar transformações antes que a requisição alcance route handlers. Use middleware para:

  • Bloquear IPs ou ranges não permitidos
  • Normalizar caminhos (ex: remover trailing slash)
  • Adicionar headers internos que route handlers esperam
// middleware.ts — filtro simples para rota de webhook
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(req: NextRequest) {
  const url = req.nextUrl.clone()
  if (url.pathname.startsWith('/api/webhook')) {
    const origin = req.headers.get('x-forwarded-for') || req.ip
    // lógica simples: rejeita se header x-internal-secret não presente
    if (!req.headers.get('x-internal-secret')) {
      return new NextResponse('forbidden', { status: 403 })
    }
  }
  return NextResponse.next()
}

export const config = { matcher: ['/api/webhook'] }

O que acontece: middleware rejeita requisições sem header interno. Isso não substitui HMAC, mas adiciona uma camada extra para tráfego vindo de integrações internas ou proxies.

Erros comuns e armadilhas

  • Expor secret no client: muitas bibliotecas e tutoriais colocam o secret em env vars client-side por conveniência.
  • Revalidar caminho genérico: chamar revalidatePath('/') para tudo causa cache-thrashing.
  • Não verificar origem do webhook: confiar unicamente no header CORS ou IP pode ser insuficiente.
  • Executar trabalho pesado no handler: revalidação deve ser rápida; tarefas longas devem ir para filas (ex: publicar assets) e disparar webhook quando pronto.
Atenção: revalidatePath e revalidateTag não bloqueiam se o caminho não existir. Certifique-se de tratar falhas para evitar falsos positivos.

Conclusão prática

Resumo direto: não exponha endpoints de revalidate públicos. Use Server Actions para revalidações iniciadas pelo editor/painel, e use route handlers com HMAC para webhooks de terceiros. Adicione middleware para filtragem inicial e aplique rate-limiting e logs. Prefira revalidatePath/revalidateTag de forma granular — revalidar só o necessário reduz custo e latência.

Checklist prático para aplicar em produção:

  1. Criar route handler HMAC-signed para webhooks; testar com script que gera assinatura.
  2. Implementar Server Action para revalidações acionadas pelo painel (autorização server-side).
  3. Adicionar middleware para bloqueio básico e normalização de requests.
  4. Monitorar e aplicar rate-limits na borda; logar cada revalidação com contexto.
  5. Testar local e em staging: unit + integração (curl/script) + testar falhas de assinatura.

Implementações detalhadas acima são funcionais em Next.js App Router (13.4+). Ajuste o uso de revalidatePath/revalidateTag conforme versão do Next.js. Aplicando essas práticas você reduz superfície de ataque, evita cache-thrashing e ganha controle preciso sobre quando e o que revalidar.

Este artigo assumiu que você já conhece App Router e Server Actions. As amostras convertem facilmente em TypeScript nos projetos que usam tsconfig com "experimentalServerActions": true. Em code reviews eu foco em autorizações, granularidade de revalidação e não exposição de segredos — siga esses princípios e você estará no caminho certo.

Assuntos: Next.js, app-router, server-actions, isr, webhook, middleware

Leia também