Todos os artigos
Next.jsApp Routerwebhook debugginglocal testing

Webhooks do Next.js no localhost, do jeito certo

A maior fonte de handlers de webhook quebrados no Next.js App Router é o corpo cru. O App Router está ok. Os route handlers estão ok. Mas as convenções para pegar o corpo não parseado mudaram entre Pages Router e App Router, e a maioria das integrações de Stripe / GitHub / Shopify copia trechos do mundo antigo. Este artigo é o mundo novo.

A regra de duas linhas

Num route handler do App Router, pegue o corpo cru com await request.text(). Não use request.json() antes da verificação de assinatura. Não tente desativar o bodyParser como fazia no Pages Router — esse flag não existe mais.

// app/api/webhooks/stripe/route.ts
import Stripe from 'stripe';
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function POST(request: Request) {
  const body = await request.text();   // raw, not parsed
  const signature = (await headers()).get('stripe-signature')!;

  let event;
  try {
    event = stripe.webhooks.constructEvent(
      body,
      signature,
      process.env.STRIPE_WEBHOOK_SECRET!,
    );
  } catch (err) {
    return NextResponse.json(
      { error: 'invalid signature' },
      { status: 400 },
    );
  }

  // Handle the event
  switch (event.type) {
    case 'checkout.session.completed':
      // ...
      break;
  }

  return NextResponse.json({ received: true });
}

Três linhas merecem atenção: request.text() te dá a string crua que o Stripe assinou. headers() no App Router é async — note o await. E a verificação de assinatura tem que acontecer antes de qualquer equivalente a JSON.parse.

O hábito de Pages Router para desaprender

Se você migrou do Pages Router, lembra de escrever isto:

// pages/api/webhooks/stripe.ts (OLD)
export const config = { api: { bodyParser: false } };

Essa config não existe no App Router. O corpo é lido sob demanda do objeto Request. O que você ler em segundo lugar está vazio — request.text() depois de request.json() não retorna nada. Escolha um, e para webhooks sempre escolha text().

Configuração do túnel para o dev do Next.js

  1. Rode next dev (ou npm run dev) — geralmente porta 3000.
  2. Em outro terminal, npx portpreview 3000.
  3. Cole a URL HTTPS na configuração de webhook do seu provedor. Para Stripe: Developers → Webhooks → Add endpoint.
  4. Dispare um evento de teste. A requisição chega ao seu route handler com todos os headers intactos.

Se você usa next dev --turbo, isso ainda funciona. O Turbopack muda o bundler, não o modelo request/response.

Edge runtime: não

Se você definiu export const runtime = 'edge' num route handler de webhook, vai perder acesso a APIs de crypto só-Node que alguns SDKs de provedores usam. O SDK Node do Stripe funciona no edge em versões recentes, mas a verificação de assinatura com provedores arbitrários é incerta. Mantenha as rotas de webhook no runtime Node a menos que tenha um motivo específico.

export const runtime = 'nodejs'; // explicit, safe default for webhooks

Armadilhas do App Router que continuamos batendo

Retornar o tipo errado

Se você retorna new Response(JSON.stringify(...)) em vez de NextResponse.json(...), o header content-type pode não ser definido. Alguns provedores se importam. Use NextResponse.json() ou defina o header manualmente.

Streaming e websockets

Handlers de webhook são requisições HTTP de vida curta, mas se você tuneliza um app Next.js que também tem rotas WebSocket (por exemplo com socket.io num servidor custom), garanta que seu túnel preserve os upgrades de WebSocket. A maioria preserva; cheque a documentação.

Middleware lendo o corpo

Se você tem um middleware.ts na raiz que chama request.text() para logging ou auth, sua rota de webhook nunca vê o corpo. Pule o caminho do webhook no seu matcher ou leia corpos só nas rotas que precisam.

Os deploys na Vercel não vão te salvar

Bugs de webhook que dependem do parse do corpo cru se comportam de forma idêntica na Vercel e no localhost. Uma vez que seu teste local em túnel passa na verificação de assinatura, a versão deployada também passará — a menos que você esteja batendo num limite de memória ou timeout do lado serverless, caso em que o sintoma é diferente (5xx, não 401).

Para detalhes do Stripe veja o guia de teste local de webhooks do Stripe. Para a matemática da assinatura em geral, o guia de verificação de assinatura. Entre na lista de espera do PortPreview para obter um túnel amigável ao Next.js com captura.

Perguntas frequentes

Como pego o corpo cru num webhook do Next.js App Router?
Chame await request.text() dentro do handler POST. Não chame request.json() primeiro — uma vez que o corpo foi lido, chamar .text() ou .json() uma segunda vez não retorna nada.
Preciso de bodyParser: false no Next.js App Router?
Não. Essa config era um padrão do Pages Router e não tem equivalente no App Router. O corpo é lido sob demanda do objeto Request, e você o lê como texto cru sempre que precisa verificar uma assinatura.
Posso rodar uma rota de webhook no edge runtime do Next.js?
Pode, mas os SDKs de verificação de assinatura às vezes dependem de APIs de crypto do Node que não estão disponíveis no edge. Fique com o runtime Node padrão para webhooks a menos que tenha um motivo específico para mudar.