en
· 4 min de leitura

Projetando APIs que podem ser usadas errado sem estrago

Clientes fazem retry, duplicam, mandam dado velho e chamam fora de ordem. Projete para que o uso errado comum seja inofensivo.

system-designengineering

O cliente da sua API não é malicioso. É pior: é comum. Ele faz retry quando a rede pisca. Manda a mesma request duas vezes porque o usuário deu duplo clique. Segura uma cópia velha de um registro por uma hora e depois grava de volta. Chama o passo três antes do passo dois porque alguém refatorou o app mobile. Nada disso é ataque, e tudo isso vai acontecer esta semana.

Uma API é bem projetada quando o uso errado comum é inofensivo. Essa é a régua, e ela é basicamente uma lista de decisões.

Assuma que toda request vai chegar duas vezes

Qualquer operação que cria ou cobra algo deveria aceitar uma chave de idempotência: um identificador gerado pelo cliente e enviado num header. O servidor guarda a chave com o resultado por uma janela, 24 horas é uma escolha comum, e devolve o resultado guardado se vir a chave de novo. O segundo clique, o POST repetido, o webhook reenviado, tudo colapsa num efeito só. Rejeite uma chave reutilizada com payload diferente em vez de servir a resposta antiga em silêncio; isso é um bug do cliente que você quer ver aparecer.

Para updates, use escritas condicionais. Devolva uma versão ou ETag em toda leitura e exija na escrita: If-Match no HTTP, ou uma coluna de versão no corpo. Uma escrita com dado velho então falha com 409 ou 412 em vez de sobrescrever uma mudança mais nova. O último que escreve vencer é uma escolha; faça de forma consciente, não por omissão.

Recuse ambiguidade, exija intenção

Se um campo pode ser interpretado de dois jeitos, rejeite. Data sem fuso, valor sem moeda, booleano mandado como a string 'false', um null que pode significar 'limpe' ou 'deixe como está'. Erro de validação é barato. Dado corrompido por interpretação adivinhada não é. Em endpoints estritos, campo desconhecido é erro, porque um erro de digitação no nome do campo ignorado em silêncio é o bug mais difícil de perceber.

Operações perigosas devem exigir intenção. Excluir uma conta, apagar um dataset, emitir reembolso em massa: faça em dois passos. A primeira chamada devolve um token de confirmação e um resumo do que vai acontecer, a segunda carrega o token. Ou exija que o cliente repita o nome do recurso. Um retry da primeira chamada é inofensivo; a segunda não se alcança por acidente. Soft delete com janela de recuperação cobre o resto.

Versione explicitamente, pagine com confiança

Coloque a versão em algum lugar em que o cliente precise escolhê-la: um segmento do path ou um header obrigatório. Uma API sem versão é uma versão 1 que você nunca vai poder mudar. Quando quebrar algo, publique uma versão nova e mantenha a antiga por um período declarado, com um header de deprecação em toda resposta.

Paginação é onde o uso errado se esconde. Paginação por offset numa coleção viva pula ou duplica itens sempre que algo é inserido ou removido entre páginas. Use paginação por keyset: um cursor opaco codificando a última chave de ordenação vista, com uma ordenação estável que inclui um desempate único. O cliente não consegue calcular o cursor, não consegue pular, e recebe cada item exatamente uma vez mesmo com a tabela mudando.

Erros que dizem o que fazer, limites que dizem quando

Uma resposta de erro deve dizer o que estava errado, em qual campo e o que fazer, num formato legível por máquina: um código estável, uma mensagem humana, o caminho do campo. Um '400 Bad Request' sem corpo desperdiça uma hora do dia de alguém. Um 429 deve trazer Retry-After. Um 503 também. E 5xx deve ficar reservado para o que o cliente não consegue corrigir, para que a política de retry seja simples: repita 5xx e 429 com backoff, nunca repita 4xx.

Rate limits pertencem a todo endpoint público, por cliente e por recurso onde fizer diferença, com os limites documentados e devolvidos em headers. Eles te protegem de bugs tanto quanto de abuso: um cliente preso num loop de retry é idêntico a um atacante.

Seguro por padrão

Todo default deve ser o que causa menos estrago. Listas devolvem uma página pequena, não tudo. Filtros omitidos são inclusivos, nunca destrutivos. Timeouts existem. Booleanos opcionais assumem o comportamento conservador. Um cliente novo que manda o mínimo deve receber um resultado seguro e sem graça. Se usar a API errado exige esforço, você projetou bem.