E-commerce com Next.js: carrinho e pagamentos com Stripe do início ao fim

É a 27ª vez que você confere a configuração do Webhook do Stripe. O cliente reclama que o dinheiro já saiu da conta, mas o pedido continua como “aguardando pagamento”. No ambiente de teste tudo funcionava; por que quebrou justamente depois da implantação?
Quando fiz meu primeiro projeto de e-commerce com Next.js, achei que a parte mais difícil seria montar a interface e escrever os estilos. Na prática, o gerenciamento do carrinho, a integração do pagamento e o fluxo do pedido tinham armadilhas em todas as etapas. Redux parecia pesado demais, Context API tinha problemas de desempenho, a documentação do Stripe estava toda em inglês e Webhook era um conceito completamente novo para mim.
O mais frustrante era que os tutoriais encontrados na internet explicavam apenas o carrinho ou apenas o pagamento. Quase nenhum mostrava o fluxo completo. As dúvidas acabavam sendo sempre as mesmas: “Qual biblioteca de estado devo usar?”, “Para que serve um Webhook?” e “Como alinhar o status do pedido ao status do pagamento?”
Vamos resolver essas partes em conjunto. Usaremos Zustand para gerenciar o estado do carrinho, por ser leve e prático, Stripe para os pagamentos, por ser uma solução amplamente usada, e Webhook para processar os pedidos, por ser o único caminho confiável. Cada etapa inclui código completo para você adaptar ao projeto.
A melhor parte é ver o fluxo inteiro funcionar pela primeira vez: pedido criado, estoque atualizado e confirmação enviada.
Por que usar Zustand para gerenciar o carrinho?
Como escolher uma biblioteca de estado em 2025
Escolher uma biblioteca de gerenciamento de estado pode ser cansativo. A documentação do Redux parece um dicionário, há muitos relatos sobre problemas de desempenho da Context API e o Zustand pode parecer novo demais. Eu também fiquei alternando entre os três até comparar melhor os cenários de uso.
Desde 2021, o Zustand se tornou uma das bibliotecas de estado para React com crescimento mais rápido em estrelas. Em 2025, seus princípios já estavam bem estabelecidos: abordagem funcional, integração natural com hooks e uma API simples. Mais importante, sua curva de aprendizado é suave, sem exigir que você domine de saída vários conceitos do Redux, como action, reducer, dispatch e middleware.
Então, qual escolher?
- Projeto pequeno, com <10 páginas: Context API é suficiente
- Projeto médio, com 10 a 50 páginas: Zustand é leve e atende bem
- Projeto grande, com mais de 50 páginas e várias equipes: Redux Toolkit oferece uma cadeia de ferramentas mais completa
O carrinho combina bem com o Zustand. Ele precisa ser compartilhado entre componentes, porque lista de produtos, ícone do carrinho e checkout usam os mesmos dados; precisa de persistência, para não perder os itens ao atualizar a página; e precisa de bom desempenho, atualizando apenas os componentes relacionados. O Zustand resolve esses três pontos com muito menos código que o Redux.
Implementação do carrinho com Zustand
Comece instalando a dependência:
npm install zustand
Depois, crie a Store do carrinho em /store/cartStore.js:
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export const useCartStore = create(
persist(
(set, get) => ({
// Estado
items: [], // [{ id, name, price, quantity, image }]
// Propriedades calculadas
get total() {
return get().items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
get count() {
return get().items.reduce((sum, item) => sum + item.quantity, 0)
},
// Métodos
addItem: (product) => set((state) => {
const existing = state.items.find(item => item.id === product.id)
if (existing) {
// O produto já existe: aumente a quantidade em 1
return {
items: state.items.map(item =>
item.id === product.id
? { ...item, quantity: item.quantity + 1 }
: item
)
}
} else {
// Produto novo: adicione-o ao carrinho
return { items: [...state.items, { ...product, quantity: 1 }] }
}
}),
removeItem: (productId) => set((state) => ({
items: state.items.filter(item => item.id !== productId)
})),
updateQuantity: (productId, quantity) => set((state) => ({
items: state.items.map(item =>
item.id === productId ? { ...item, quantity } : item
)
})),
clearCart: () => set({ items: [] })
}),
{
name: 'shopping-cart', // Chave do localStorage
}
)
)
Apesar de extenso à primeira vista, o código é simples. O array items armazena os produtos, enquanto total e count são propriedades calculadas para o valor e a quantidade total. Os demais métodos adicionam, removem ou atualizam itens. O middleware persist salva os dados automaticamente no localStorage, evitando que o carrinho seja perdido ao atualizar a página.
O uso nos componentes também é direto:
import { useCartStore } from '@/store/cartStore'
function ProductCard({ product }) {
const addItem = useCartStore(state => state.addItem)
return (
<button onClick={() => addItem(product)}>
Adicionar ao carrinho
</button>
)
}
function CartIcon() {
const count = useCartStore(state => state.count)
return <div>Carrinho ({count})</div>
}
Observe que useCartStore(state => state.addItem) é um seletor. Ele assina apenas o método addItem, então mudanças nos demais dados do carrinho não provocam uma nova renderização. Essa assinatura precisa é uma das razões para o bom desempenho do Zustand.
Quem já usou useSelector e useDispatch no Redux perceberá que o Zustand exige bem menos código. Não é preciso declarar action types nem reducers; os métodos ficam diretamente na Store.
E se o projeto já usa Redux? Não há necessidade de migrar. O Redux Toolkit continua sendo uma boa opção. O projeto de e-commerce open source C-Shopping, por exemplo, usa Redux Toolkit com RTK Query e consegue manter o fluxo de dados rastreável e estável. Para projetos novos, porém, costumo preferir Zustand por ser mais fácil de aprender e permitir entregas rápidas.
Fluxo completo de pagamentos com Stripe
Entenda primeiro o fluxo de pagamento do Stripe
Ao ler a documentação do Stripe pela primeira vez, surgem várias perguntas: o que é uma Checkout Session? E um Payment Intent? Por que é preciso abrir uma página do Stripe? O pagamento não pode acontecer dentro do próprio site?
Visto de ponta a ponta, o fluxo é claro:
- Frontend: o usuário clica em “Pagar” e chama sua API para criar uma Checkout Session
- Backend: cria a Session e retorna um
session.id - Frontend: recebe o
session.ide usa Stripe.js para abrir a página de pagamento hospedada pelo Stripe - Usuário: informa os dados do cartão na página do Stripe e conclui o pagamento
- Stripe: envia um Webhook ao seu backend quando o pagamento é concluído
- Backend: recebe o Webhook, cria o pedido, baixa o estoque e envia o e-mail
- Stripe: redireciona o usuário de volta para o seu site pela
success_url
O ponto principal é: nunca processe o sucesso do pagamento no frontend. O usuário pode fechar o navegador depois de pagar, a conexão pode cair ou ele pode não concluir o redirecionamento. O único método confiável é o Webhook, que veremos em detalhes adiante.
Criar uma Stripe Checkout Session
Instale as dependências:
npm install stripe @stripe/stripe-js
Depois, configure as variáveis em .env.local:
STRIPE_SECRET_KEY=sk_test_xxxxx # Usada no backend; nunca exponha no frontend
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx # Usada no frontend
STRIPE_WEBHOOK_SECRET=whsec_xxxxx # Usada para validar a assinatura do Webhook
Crie a rota de API em /pages/api/create-checkout.js:
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Método não permitido' })
}
try {
const { items } = req.body // Dados do carrinho
// Cria line_items no formato exigido pelo Stripe
const lineItems = items.map(item => ({
price_data: {
currency: 'usd',
product_data: {
name: item.name,
images: [item.image],
},
unit_amount: Math.round(item.price * 100), // O Stripe usa a menor unidade monetária
},
quantity: item.quantity,
}))
// Cria a Checkout Session
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: lineItems,
mode: 'payment', // Pagamento único; use 'subscription' para assinaturas
success_url: `${req.headers.origin}/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${req.headers.origin}/cart`,
metadata: {
// Dados personalizados que ficarão disponíveis no Webhook
userId: req.user?.id || 'guest',
},
})
res.status(200).json({ sessionId: session.id })
} catch (err) {
console.error('Falha ao criar a Checkout Session:', err)
res.status(500).json({ error: err.message })
}
}
Preste atenção nestes detalhes:
- Multiplique
unit_amountpor 100, pois o Stripe usa a menor unidade monetária: US$ 99,99 correspondem a 9.999 centavos {CHECKOUT_SESSION_ID}emsuccess_urlé um marcador que o Stripe substitui pelosession_idrealmetadatapode guardar seus próprios dados, como ID do usuário e observações do pedido, para uso posterior no Webhook
Chamar o Checkout no frontend
Na página de finalização, em /pages/checkout.js:
import { loadStripe } from '@stripe/stripe-js'
import { useCartStore } from '@/store/cartStore'
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY)
export default function CheckoutPage() {
const { items, total } = useCartStore()
const handleCheckout = async () => {
try {
// Chama a API do backend para criar a Session
const response = await fetch('/api/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items }),
})
const { sessionId } = await response.json()
// Abre a página de pagamento do Stripe
const stripe = await stripePromise
const { error } = await stripe.redirectToCheckout({ sessionId })
if (error) {
console.error('Falha ao abrir a página de pagamento:', error)
alert(error.message)
}
} catch (err) {
console.error('Falha ao iniciar o pagamento:', err)
alert('Não foi possível iniciar o pagamento. Tente novamente mais tarde.')
}
}
return (
<div>
<h1>Finalizar compra</h1>
{items.map(item => (
<div key={item.id}>
{item.name} x {item.quantity} = ${item.price * item.quantity}
</div>
))}
<div>Total: ${total}</div>
<button onClick={handleCheckout}>Pagar</button>
</div>
)
}
Depois que o usuário clicar em “Pagar”, ele será redirecionado para a página hospedada pelo Stripe. O Stripe fornece o formulário e cuida da validação do cartão e da detecção de fraude, evitando que você implemente tudo isso do zero.
É possível personalizar essa página? Sim. O Stripe permite mudar cores, logo e fontes, mas mantém o layout geral. Para controlar toda a interface, você pode usar Stripe Elements e incorporar o formulário no frontend, embora isso aumente bastante a complexidade e não seja a melhor opção para iniciantes.
Redirecionar depois do pagamento
Depois que o usuário conclui o pagamento, o Stripe o redireciona para a success_url. Essa página pode exibir os detalhes do pedido:
// /pages/success.js
import { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
export default function SuccessPage() {
const router = useRouter()
const { session_id } = router.query
const [order, setOrder] = useState(null)
useEffect(() => {
if (session_id) {
// Busca os dados do pedido no backend
fetch(`/api/order?session_id=${session_id}`)
.then(res => res.json())
.then(data => setOrder(data))
}
}, [session_id])
if (!order) return <div>Carregando...</div>
return (
<div>
<h1>Pagamento concluído!</h1>
<p>Número do pedido: {order.id}</p>
<p>Valor: ${order.total}</p>
</div>
)
}
Lembre-se: essa página serve apenas para exibir informações. A criação real do pedido deve ocorrer no Webhook. Agora podemos implementar essa parte.
Processamento do pedido e sincronização de status com Webhook
Por que o Webhook é tão importante?
Na primeira vez que implementei pagamentos, achei que o redirecionamento para a página de sucesso já confirmava o pagamento e coloquei ali toda a lógica do pedido. Durante os testes, o usuário fechou o navegador logo depois de pagar. O pedido não foi criado e nem ficou claro se o dinheiro seria devolvido.
Foi então que a documentação do Stripe deixou o ponto central evidente: o Webhook é a única forma confiável de processar pedidos. Há três razões:
- O redirecionamento do usuário não é confiável: ele pode fechar o navegador, perder a conexão ou não clicar para concluir
- A segurança exige o backend: operações sensíveis, como criar pedidos, baixar o estoque e iniciar o envio, não podem ficar sob controle do frontend
- É a recomendação do Stripe: toda lógica essencial do negócio deve ser executada no Webhook
Em termos simples, o Webhook é uma chamada iniciada pelo servidor do Stripe ao seu servidor. Ela avisa que um pagamento foi concluído ou que uma assinatura foi cancelada, e seu sistema executa o processamento correspondente.
Criar o endpoint do Webhook
No Next.js, crie /pages/api/stripe-webhook.js:
import Stripe from 'stripe'
import { buffer } from 'micro'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY)
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET
// Configuração essencial: desativa o parsing padrão do body no Next.js
export const config = {
api: {
bodyParser: false,
},
}
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).send('Método não permitido')
}
const buf = await buffer(req)
const sig = req.headers['stripe-signature']
let event
try {
// Valida a assinatura do Webhook; esta etapa é essencial
event = stripe.webhooks.constructEvent(buf, sig, webhookSecret)
} catch (err) {
console.error('Falha ao validar a assinatura do Webhook:', err.message)
return res.status(400).send(`Webhook Error: ${err.message}`)
}
// Processa os diferentes tipos de evento
switch (event.type) {
case 'checkout.session.completed':
await handleCheckoutSessionCompleted(event.data.object)
break
case 'payment_intent.succeeded':
await handlePaymentIntentSucceeded(event.data.object)
break
case 'invoice.payment_failed':
await handleInvoicePaymentFailed(event.data.object)
break
default:
console.log(`Tipo de evento não tratado: ${event.type}`)
}
res.status(200).json({ received: true })
}
async function handleCheckoutSessionCompleted(session) {
console.log('Pagamento concluído!', session.id)
// Obtém os dados do carrinho por metadata ou usando session.id
const userId = session.metadata.userId
const sessionId = session.id
const total = session.amount_total / 100 // Converte o valor de volta para dólares
// Verifica se o pedido já existe para garantir a idempotência
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})
if (existingOrder) {
console.log('Pedido já existe; criação ignorada')
return
}
// Cria o pedido
const order = await db.order.create({
data: {
userId,
stripeSessionId: sessionId,
status: 'paid',
total,
// ... Outros campos
}
})
// Baixa o estoque
await updateInventory(order.items)
// Envia o e-mail de confirmação
await sendOrderConfirmationEmail(userId, order)
console.log('Pedido criado:', order.id)
}
async function handlePaymentIntentSucceeded(paymentIntent) {
// Confirma que o pagamento foi recebido
console.log('Pagamento confirmado:', paymentIntent.id)
}
async function handleInvoicePaymentFailed(invoice) {
// Trata a falha no pagamento de uma assinatura
console.log('Falha no pagamento:', invoice.id)
// Envia um lembrete, suspende o serviço etc.
}
Há três pontos essenciais:
- Desative o bodyParser: o Stripe precisa do corpo original da requisição, ou raw body, para validar a assinatura. Se o Next.js processar o body antes, a validação falhará
- Valide a assinatura:
stripe.webhooks.constructEvent()confirma que a requisição veio do Stripe e impede falsificações de terceiros - Implemente idempotência: o Stripe pode repetir um Webhook por causa da rede ou do mecanismo de novas tentativas. Use
stripeSessionIdcomo índice único para impedir que o mesmo pagamento crie vários pedidos
Testar o Webhook localmente
O Stripe não consegue chamar diretamente o localhost, então o desenvolvimento local exige a Stripe CLI. Primeiro, instale-a:
# macOS
brew install stripe/stripe-cli/stripe
# Windows, com Scoop
scoop install stripe
# Ou faça o download no site oficial
# https://stripe.com/docs/stripe-cli
Autentique-se e inicie o encaminhamento do Webhook:
stripe login
stripe listen --forward-to localhost:3000/api/stripe-webhook
A CLI exibirá um webhook secret temporário, parecido com whsec_xxxxx. Copie-o para .env.local:
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
Em outro terminal, dispare um evento de teste:
stripe trigger checkout.session.completed
Os logs aparecerão tanto na CLI quanto no console do Next.js, confirmando que o Webhook foi recebido. A partir daí, você pode depurar a criação do pedido.
Fiquei preso nessa etapa por um bom tempo porque a validação da assinatura sempre falhava. O bodyParser do Next.js ainda estava ativo e alterava o body antes da verificação. Não se esqueça de adicionar export const config.
Gerenciar o status do pedido
O fluxo de status pode ser representado assim:
Aguardando pagamento → Pago → Em separação → Enviado → Concluído
↓
Cancelado/Reembolsado
Armazene o status como enum no banco de dados:
// schema.prisma
model Order {
id String @id @default(cuid())
stripeSessionId String @unique // Garantia de idempotência
userId String
status OrderStatus @default(PENDING)
total Float
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
enum OrderStatus {
PENDING // Aguardando pagamento
PAID // Pago
PREPARING // Em separação
SHIPPED // Enviado
COMPLETED // Concluído
CANCELLED // Cancelado
REFUNDED // Reembolsado
}
Quando o Webhook receber checkout.session.completed, defina o status como PAID. Os estados seguintes, como envio e conclusão, podem ser atualizados manual ou automaticamente pelo seu sistema administrativo.
Tratar erros
O Webhook pode falhar, por exemplo, se o banco de dados ficar indisponível ou um serviço externo sair do ar. O Stripe possui novas tentativas automáticas, mas você deve registrar a falha:
async function handleCheckoutSessionCompleted(session) {
try {
// Lógica do negócio
} catch (error) {
console.error('Falha ao processar o pedido:', error)
// Registra o erro em um sistema como Sentry ou LogRocket
await logError({
type: 'webhook_error',
event: 'checkout.session.completed',
sessionId: session.id,
error: error.message,
})
throw error // Propaga o erro para que o Stripe faça uma nova tentativa
}
}
Se o Webhook falhar, o Stripe tentará novamente por três dias. Nesse período, você também pode consultar o Webhook com falha no Stripe Dashboard e reenviá-lo manualmente.
Fluxo completo de um pedido
Com todas as peças prontas, podemos conectá-las e acompanhar o percurso completo de um pedido.
Caminho completo da compra
- Página do produto: o usuário clica em “Adicionar ao carrinho”, a Store do Zustand é atualizada e o número no ícone aumenta
- Página do carrinho: o usuário confere os itens, ajusta as quantidades e clica em “Finalizar compra”
- Página de finalização: mostra o resumo do pedido e o botão “Pagar”
- Frontend: chama
/api/create-checkoute envia os dados do carrinho - Backend: cria a Session do Stripe e retorna
sessionId - Frontend: abre a página de pagamento hospedada pelo Stripe
- Usuário: informa o cartão e clica em “Pay”
- Stripe: processa o pagamento e, quando ele é concluído, envia um Webhook para
/api/stripe-webhook - Webhook no backend: valida a assinatura → cria o pedido → baixa o estoque → envia o e-mail
- Stripe: redireciona o usuário para
/success?session_id=xxx - Frontend: a página de sucesso chama
/api/order?session_id=xxxe mostra os detalhes do pedido
O fluxo parece complexo, mas cada etapa tem uma responsabilidade clara. O ponto decisivo é que a etapa 9 deve acontecer no Webhook, sem depender da etapa 11.
Pontos importantes do modelo de dados
model Order {
id String @id @default(cuid())
stripeSessionId String @unique // Idempotência
userId String
status OrderStatus @default(PENDING)
total Float
items OrderItem[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id])
}
model OrderItem {
id String @id @default(cuid())
orderId String
productId String
quantity Int
price Float // Preço no momento da compra, sem alterações futuras
order Order @relation(fields: [orderId], references: [id])
product Product @relation(fields: [productId], references: [id])
}
O campo price de OrderItem guarda o preço no momento da compra, não o preço atual do Product relacionado. Assim, mesmo que o produto fique mais caro depois, o pedido antigo continua com o valor original.
Tratar casos de borda
1. O que fazer quando não há estoque suficiente?
Verifique o estoque antes de criar a Checkout Session:
// /pages/api/create-checkout.js
const { items } = req.body
// Verifica o estoque
for (const item of items) {
const product = await db.product.findUnique({ where: { id: item.id } })
if (product.stock < item.quantity) {
return res.status(400).json({ error: `Estoque insuficiente de ${product.name}` })
}
}
// Com estoque suficiente, prossiga com a criação da Session...
2. O pagamento foi concluído, mas o Webhook falhou. E agora?
O Stripe tentará novamente por três dias. Você também pode reenviar o Webhook manualmente pelo Stripe Dashboard. Outra opção é criar uma tarefa agendada que procure periodicamente Sessions com pagamento concluído, mas sem pedido correspondente, para fazer a reconciliação.
3. O cliente pagou, mas o envio atrasou.
Confira o estoque mais uma vez antes de enviar. Se não houver produto disponível, entre em contato com o cliente para oferecer reembolso ou troca.
Cuidados na implantação em produção
O ambiente de testes estar funcionando não significa que a implantação possa ser feita sem outras verificações. Alguns pontos merecem atenção.
Configurar as variáveis de ambiente
As chaves de produção são diferentes das chaves de teste:
# .env.production
STRIPE_SECRET_KEY=sk_live_xxxxx # A chave é live, não test
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx # Webhook Secret de produção
Ao implantar na Vercel ou em outra plataforma, configure as variáveis de ambiente. Nunca envie a Secret Key para o Git.
Configurar o endpoint do Webhook
No ambiente de teste, a Stripe CLI encaminha os eventos. Em produção, o endpoint precisa ser cadastrado manualmente no Stripe Dashboard:
- Entre no Stripe Dashboard
- Acesse “Developers” → “Webhooks”
- Clique em “Add endpoint”
- Informe a URL de produção:
https://yourdomain.com/api/stripe-webhook - Selecione os eventos, como
checkout.session.completedepayment_intent.succeeded - Salve, copie o Signing secret, parecido com
whsec_xxxxx, e adicione-o às variáveis de ambiente
Na minha primeira implantação, esqueci essa configuração. Os pedidos não eram criados porque nenhum Webhook chegava ao servidor.
Checklist de segurança
Antes da implantação, confirme todos os itens:
- ✅ Toda a lógica de pagamento é executada no backend; o frontend apenas redireciona
- ✅ A assinatura do Webhook é validada com
stripe.webhooks.constructEvent - ✅ O valor do pagamento é comparado com o valor do pedido para impedir alterações de preço no frontend
- ✅ A idempotência está implementada com um índice único em
stripeSessionId - ✅ Todos os eventos de pagamento são registrados em logs, como Sentry ou Datadog
- ✅ Há alertas para anomalias na taxa de falhas do Webhook e na taxa de pagamentos concluídos
O terceiro item é especialmente importante. Mesmo que o backend defina o preço ao criar a Session, ainda é preciso validá-lo no Webhook. Segurança não comporta atalhos.
Monitoramento e alertas
Em produção, vale integrar um sistema de monitoramento:
// /pages/api/stripe-webhook.js
import * as Sentry from '@sentry/nextjs'
export default async function handler(req, res) {
try {
// ... Lógica do Webhook
} catch (error) {
Sentry.captureException(error, {
tags: {
type: 'stripe_webhook',
event: event.type,
},
})
throw error
}
}
Monitore principalmente:
- Taxa de falhas do Webhook; emita um alerta acima de 5%
- Taxa de pagamentos concluídos; uma queda repentina pode indicar indisponibilidade do Stripe ou erro de configuração
- Tempo de criação do pedido; investigue quando passar de três segundos
Resumo: do primeiro passo à produção
Os pontos principais são:
- Estado do carrinho: use Zustand, por ser leve, ou Redux Toolkit em projetos grandes; o middleware persist cuida da persistência
- Integração de pagamentos: crie uma Stripe Checkout Session e use a página hospedada para evitar a implementação do formulário e da validação do cartão
- Processamento do pedido: crie o pedido, baixe o estoque e envie o e-mail dentro do Webhook
- Implantação em produção: configure as variáveis de ambiente, cadastre o endpoint do Webhook e adicione monitoramento e alertas
Guarde três princípios:
- A lógica de pagamento deve ficar no backend: o frontend não é confiável
- O Webhook é a única fonte confiável: não dependa do redirecionamento do usuário
- A segurança vem primeiro: valide assinaturas, evite duplicidades e registre logs
Se este é seu primeiro pagamento de e-commerce, faça todo o fluxo no ambiente de teste do Stripe. Use o cartão 4242 4242 4242 4242; qualquer data de validade futura e qualquer CVV servem. Migre para produção apenas depois de verificar o processo completo.
Para avançar, consulte:
- Documentação oficial do Stripe: https://stripe.com/docs, detalhada apesar de estar em inglês
- Tutorial completo de Next.js com Stripe: guia de Pedro Alonso para 2025; procure por “Stripe Next.js 15 complete guide”
- Projeto open source: a plataforma de e-commerce C-Shopping, criada com Redux Toolkit e Stripe
Quando o primeiro pedido for criado automaticamente, o estoque for atualizado e o cliente receber o e-mail de confirmação, todo o trabalho de conectar essas peças fará sentido.
Como implementar um carrinho e pagamentos com Stripe no Next.js
Etapas detalhadas para criar do zero um carrinho e um sistema de pagamentos, do gerenciamento de estado ao processamento dos pedidos
⏱️ Estimated time: 2 hr
- 1
Step 1: Instalar as dependências e configurar o carrinho com Zustand
Instale a biblioteca de gerenciamento de estado Zustand:
• npm install zustand
Crie a Store do carrinho (/store/cartStore.js):
• Defina o array items para armazenar os produtos
• Adicione as propriedades calculadas total e count
• Implemente os métodos addItem, removeItem, updateQuantity e clearCart
• Use o middleware persist para salvar os dados no localStorage
Configurações importantes:
• O middleware persist faz a persistência automática, evitando a perda de dados ao atualizar a página
• Use seletores nas assinaturas (useCartStore(state => state.addItem)) para evitar novas renderizações desnecessárias
Cenário indicado: projetos pequenos e médios, com 10 a 50 páginas, que precisam de gerenciamento de estado leve - 2
Step 2: Criar a API da Stripe Checkout Session
Instale as dependências do Stripe:
• npm install stripe @stripe/stripe-js
Configure as variáveis de ambiente (.env.local):
• STRIPE_SECRET_KEY=sk_test_xxxxx (usada no backend; não pode vazar)
• NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxxxx (usada no frontend)
• STRIPE_WEBHOOK_SECRET=whsec_xxxxx (usada para validar a assinatura do Webhook)
Crie a rota de API (/pages/api/create-checkout.js):
• Receba os dados de items do carrinho
• Converta-os para o formato line_items do Stripe, lembrando que unit_amount deve ser multiplicado por 100
• Crie checkout.sessions, definindo success_url e cancel_url
• Armazene dados personalizados, como userId, em metadata
• Retorne sessionId ao frontend
Detalhes importantes:
• O Stripe usa a menor unidade monetária, então o preço deve ser multiplicado por 100
• success_url usa o marcador {CHECKOUT_SESSION_ID}
• metadata pode guardar dados do negócio que ficarão disponíveis no Webhook - 3
Step 3: Chamar o Stripe Checkout no frontend
Implemente a página de finalização (/pages/checkout.js):
• Use loadStripe para carregar o Stripe.js
• Chame /api/create-checkout para criar a Session
• Use stripe.redirectToCheckout() para abrir a página de pagamento
Tratamento de erros:
• Capture erros de rede com catch
• Verifique o error retornado por stripe.redirectToCheckout
• Mostre uma mensagem de erro clara ao usuário
Sobre a página de pagamento:
• A página é hospedada pelo Stripe, então você não precisa criar o formulário
• Ela já trata a validação do cartão e a detecção de fraude
• É possível personalizar cores, logo e fontes
Atenção:
• Nunca processe o sucesso do pagamento no frontend
• O usuário pode fechar o navegador ou não clicar no botão de conclusão
• A criação real do pedido deve acontecer no Webhook - 4
Step 4: Configurar o endpoint do Webhook para processar pedidos
Crie a API do Webhook (/pages/api/stripe-webhook.js):
Configuração obrigatória:
• export const config = { api: { bodyParser: false } } (desativa o parsing do body)
• Use buffer(req) para obter o corpo original da requisição
• Use stripe.webhooks.constructEvent() para validar a assinatura
Tipos de evento tratados:
• checkout.session.completed: pagamento concluído; crie o pedido
• payment_intent.succeeded: confirme o recebimento do pagamento
• invoice.payment_failed: falha no pagamento de uma assinatura
Garantia de idempotência:
• Verifique se stripeSessionId já existe
• Adicione um índice unique ao banco de dados
• Evite que chamadas repetidas do Webhook criem vários pedidos
Lógica do negócio:
• Crie o registro do pedido com status 'paid'
• Baixe o estoque com updateInventory
• Envie o e-mail de confirmação com sendOrderConfirmationEmail
• Registre logs e erros
Teste local:
• stripe login (autentica a CLI)
• stripe listen --forward-to localhost:3000/api/stripe-webhook
• stripe trigger checkout.session.completed (evento de teste)
Pontos importantes:
• É obrigatório desativar o bodyParser; caso contrário, a validação da assinatura falhará
• É obrigatório validar a assinatura para impedir requisições falsificadas
• Se o Webhook falhar, o Stripe tentará novamente por três dias - 5
Step 5: Implantar em produção e configurar a segurança
Variáveis de ambiente:
• Use as chaves de produção sk_live_xxxxx e pk_live_xxxxx
• Configure as variáveis de ambiente na plataforma, como Vercel ou Netlify
• Nunca envie a Secret Key para o Git
Configuração no Stripe Dashboard:
• Acesse Developers → Webhooks
• Adicione o endpoint de produção https://yourdomain.com/api/stripe-webhook
• Selecione os eventos, como checkout.session.completed
• Copie o Signing secret para as variáveis de ambiente
Checklist de segurança:
• ✅ Toda a lógica de pagamento é executada no backend
• ✅ O Webhook valida a assinatura
• ✅ O valor pago é comparado com o valor do pedido
• ✅ A idempotência é garantida por um índice único em stripeSessionId
• ✅ Todos os pagamentos são registrados em logs
• ✅ Alertas de monitoramento são emitidos quando a taxa de falhas do Webhook passa de 5%
Métricas de monitoramento:
• Taxa de falhas do Webhook
• Taxa de pagamentos concluídos
• Tempo de criação do pedido; investigue quando passar de três segundos
Integração com monitoramento:
• Use Sentry ou LogRocket para registrar erros
• Configure regras de alerta
• Consulte periodicamente os logs de Webhook no Stripe Dashboard
Fluxo de testes:
• Use o cartão de teste 4242 4242 4242 4242
• Valide o fluxo completo: carrinho → pagamento → Webhook → criação do pedido
• Teste cenários de falha, como estoque insuficiente ou falha no Webhook
FAQ
Como escolher entre Redux e Zustand para o meu projeto?
• Projeto pequeno, com menos de 10 páginas: Context API é suficiente; não é necessário adicionar outra biblioteca de estado
• Projeto médio, com 10 a 50 páginas: Zustand é mais indicado por ser leve, exigir pouco código e ter uma curva de aprendizado baixa
• Projeto grande, com mais de 50 páginas e várias equipes: Redux Toolkit oferece ferramentas maduras, boa depuração e uma comunidade consolidada
Cenários específicos:
• Projeto novo com iterações rápidas: escolha Zustand para começar e entregar mais rápido
• Projeto que já usa Redux: não há motivo para trocar; Redux Toolkit continua sendo uma boa opção
• Equipe sem experiência em gerenciamento de estado: a curva de aprendizado do Zustand é mais suave
Para carrinhos, o Zustand costuma ser uma boa escolha: ele resolve o compartilhamento entre componentes, a persistência e a otimização de desempenho.
Por que não devo processar o sucesso do pagamento no frontend?
Falta de confiabilidade:
• O usuário pode fechar o navegador logo após pagar
• Uma falha de rede pode impedir o redirecionamento
• O usuário pode não concluir deliberadamente o retorno ao site
Riscos de segurança:
• O código do frontend pode ser alterado ou contornado
• Operações sensíveis, como criar o pedido e baixar o estoque, não podem ficar expostas no frontend
• Não há como impedir que um usuário mal-intencionado falsifique o status de pagamento
Abordagem correta:
• Execute toda a lógica essencial do negócio no Webhook
• O servidor do Stripe notifica diretamente o seu backend, sem depender do navegador do usuário
• O Webhook usa validação de assinatura e é confiável
• O Stripe recomenda o Webhook como a única fonte confiável para processar pedidos
A página de sucesso no frontend serve apenas para exibição e não deve executar lógica do negócio.
O que fazer quando a validação da assinatura do Webhook sempre falha?
Causa mais frequente, em 90% dos casos:
• O bodyParser do Next.js não foi desativado
• Solução: adicione export const config = { api: { bodyParser: false } } à rota de API
Outras causas:
• Webhook Secret incorreto; confira STRIPE_WEBHOOK_SECRET em .env.local
• Chave do ambiente errado; os secrets de teste e produção são diferentes
• Corpo da requisição alterado por um middleware; verifique se algum middleware global processa o body
Etapas de depuração:
1. Confirme que bodyParser: false está configurado
2. Exiba req.headers['stripe-signature'] para verificar se o cabeçalho existe
3. Teste com a Stripe CLI: stripe listen --forward-to localhost:3000/api/stripe-webhook
4. Consulte os detalhes do erro exibidos pela CLI
5. Confirme que está usando o webhook secret temporário fornecido pela CLI
Atenção no teste local:
• No desenvolvimento local, encaminhe as requisições com a Stripe CLI
• A CLI fornece um webhook secret temporário, no formato whsec_xxxxx
• Sempre que a CLI for reiniciada, um novo secret será gerado e .env.local precisará ser atualizado
Como impedir que chamadas repetidas do Webhook criem vários pedidos?
No banco de dados:
• Adicione um índice unique ao campo stripeSessionId
• Exemplo no Prisma: stripeSessionId String @unique
• O banco rejeitará automaticamente uma inserção duplicada
No código:
• Antes de criar o pedido, consulte se ele já existe
• Use findUnique({ where: { stripeSessionId } })
• Se existir, retorne sem criar outro pedido
Exemplo:
```javascript
const existingOrder = await db.order.findUnique({
where: { stripeSessionId: sessionId }
})
if (existingOrder) {
console.log('Pedido já existe; criação ignorada')
return
}
// Crie o pedido somente quando ainda não existir
const order = await db.order.create({ ... })
```
Por que a idempotência é necessária:
• O Stripe pode enviar o mesmo Webhook mais de uma vez por causa de problemas de rede ou do mecanismo de repetição
• Seu código deve processar chamadas duplicadas com segurança
• Isso impede que o mesmo pagamento crie vários pedidos ou baixe o estoque várias vezes
Outras recomendações:
• Registre cada chamada do Webhook
• Monitore a frequência das chamadas duplicadas
• Configure alertas
Como investigar pedidos que não são criados depois da implantação em produção?
1. Confira se o Webhook foi recebido:
• Acesse Stripe Dashboard → Developers → Webhooks
• Consulte o histórico e o status das chamadas, concluídas ou com falha
• Se não houver chamada, há um problema na configuração do endpoint
2. Confira a configuração do endpoint:
• Verifique a URL https://yourdomain.com/api/stripe-webhook
• Confirme que checkout.session.completed está entre os eventos selecionados
• Confira se o endpoint está ativo
3. Confira as variáveis de ambiente:
• Verifique se STRIPE_WEBHOOK_SECRET está correto
• Confirme que o secret é de produção, não de teste
• Confira se a variável foi definida na plataforma de implantação, como Vercel ou Netlify
4. Confira o código do endpoint do Webhook:
• Verifique se o bodyParser está desativado
• Confira a validação da assinatura
• Procure registros de erro
5. Consulte os logs da aplicação:
• Verifique os logs do servidor, como Vercel Logs ou CloudWatch
• Procure a stack trace do erro
• Confirme que a função do Webhook foi executada
6. Faça um teste manual:
• Localize o Webhook com falha no Stripe Dashboard
• Clique em 'Resend' para reenviá-lo
• Observe o resultado e a mensagem de erro
Erros comuns:
• Esquecer de cadastrar o endpoint do Webhook em produção
• Usar o webhook secret do ambiente de teste
• Ter as requisições do Stripe bloqueadas pelo firewall da plataforma
Depois de corrigir:
• Faça um teste completo de pagamento com o cartão de teste
• Confirme a criação do pedido, a baixa do estoque e o envio do e-mail
18 min de leitura · Publicado em: 7 jan 2026 · Atualizado em: 4 set 2026
Guia completo Next.js
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Testes E2E no Next.js com Playwright: guia prático de automação
Aprenda a automatizar testes E2E em projetos Next.js com Playwright, Page Object Model, testes de API e integração com CI/CD, incluindo armadilhas e configurações usadas em projetos reais.
Parte 21 de 26
Próximo
Guia prático de CI/CD para Next.js: testes e deploy automáticos com GitHub Actions
Aprenda a automatizar testes e deploys de projetos Next.js com GitHub Actions, incluindo configurações completas, problemas comuns e boas práticas. Depois disso, basta fazer push para publicar uma nova versão.
Parte 23 de 26



Comentários
Entre com GitHub para comentar