Idempotência: o que acontece quando carregam duas vezes
A ligação está lenta, a pessoa carrega em "Pagar", não acontece nada visível, e carrega outra vez. Ou o telemóvel perde a rede a meio e a aplicação repete o pedido sozinha. Em qualquer dos casos, o servidor recebe dois pedidos iguais — e a pergunta é se isso faz duas encomendas.
1. O que a palavra quer dizer
Uma operação é idempotente quando repeti-la dá o mesmo resultado que fazê-la uma vez. Não é "não faz nada da segunda vez" — é "o estado final é o mesmo".
GET,PUTeDELETEsão idempotentes por definição do protocolo.POSTnão é, e é precisamente com ele que se criam coisas.
2. A chave de idempotência
Quem faz o pedido gera um identificador e envia-o. O servidor guarda-o com a resposta:
await fetch('/api/encomendas', {
method: 'POST',
headers: { 'Idempotency-Key': chaveGeradaUmaVez },
body: JSON.stringify(encomenda),
});const jaFeita = await db.pedidos.findUnique({ where: { chave } });
if (jaFeita) return jaFeita.resposta; // devolve o mesmo, sem repetir nada
const encomenda = await criarEncomenda(dados);
await db.pedidos.create({ data: { chave, resposta: encomenda } });
return encomenda;O detalhe que faz isto funcionar: a chave é gerada uma vez, quando o formulário abre — e não a cada tentativa. Se for gerada a cada envio, as duas tentativas têm chaves diferentes e voltamos ao princípio.
3. A base como última defesa
Onde houver um campo naturalmente único, o índice único resolve sem lógica nenhuma:
CREATE UNIQUE INDEX ON votos (artigo_id, votante);O segundo voto não passa, e passa a ser um erro tratável em vez de uma linha duplicada. É o mesmo princípio das chaves de idempotência, escrito no esquema.
4. E do lado do botão
// Desativar enquanto o pedido está a decorrer resolve o caso comum
<button disabled={aEnviar}>{aEnviar ? 'A enviar…' : 'Pagar'}</button>Resolve o caso comum, e não resolve o resto: uma repetição automática da rede, um clique num separador duplicado, um robô. A defesa do lado do cliente é cortesia; a do servidor é que é a defesa.