AAAbel Aguiar
← Voltar para o blog
apisbackendcontratossistemas-críticos

APIs para sistemas críticos: versionamento, contratos e rastreabilidade

Boas práticas para projetar APIs que precisam evoluir sem quebrar consumidores e sem perder capacidade de auditoria

Abel Aguiar·
APIs para sistemas críticos: versionamento, contratos e rastreabilidade

Uma API crítica raramente quebra sozinha. Ela quebra junto com uma fila, um atendimento, um fechamento financeiro, uma integração de órgão público, uma tela do operador ou um relatório que alguém precisa entregar até o fim do dia.

Já vi problema pequeno virar incidente grande por causa de um campo renomeado sem aviso. O backend passou em todos os testes internos, o deploy foi feito sem erro e, mesmo assim, um consumidor antigo começou a descartar respostas porque esperava status e passou a receber situacao. Tecnicamente era "só um nome melhor". Na prática, foi quebra de contrato.

É por isso que, em sistemas críticos, API não é apenas rota HTTP. API é compromisso entre quem oferece e quem consome. O código implementa esse compromisso, mas não deveria ser o único lugar onde ele existe.

O contrato precisa ser visível

Antes de mexer em controller, service ou migration, vale gastar tempo descrevendo o que a API promete entregar. Não como documento decorativo, mas como material que outro time consegue usar sem chamar alguém no chat a cada dúvida.

Para APIs HTTP, OpenAPI costuma ser um bom ponto de partida. Você descreve endpoints, parâmetros, schemas, respostas e exemplos. Com isso, dá para revisar contrato em pull request, gerar documentação, criar mocks e até validar quebra antes do deploy.

Um contrato pequeno já evita muita ambiguidade:

paths:
  /v1/protocolos:
    post:
      summary: Cria um protocolo de atendimento
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          schema:
            type: string
      responses:
        "201":
          description: Protocolo criado
        "409":
          description: Requisição já processada

Ferramentas como Swagger Editor, Redocly CLI e Spectral ajudam a escrever, visualizar e aplicar lint nesse contrato. A ferramenta não substitui conversa entre times, mas tira muita decisão importante da memória das pessoas.

Em API crítica, "olha o código para entender" não escala. Código mostra como funciona hoje. Contrato mostra o que você prometeu manter funcionando amanhã.

Versionamento começa antes do /v1

Colocar /v1 na URL é simples. Difícil é ter uma regra clara para decidir quando uma mudança ainda cabe na mesma versão.

Adicionar um campo opcional na resposta normalmente é seguro. Criar um endpoint novo também. O consumidor antigo continua funcionando porque nada que ele usa deixou de existir. Por outro lado, remover campo, mudar tipo, renomear atributo, alterar regra de paginação ou transformar um campo opcional em obrigatório tende a quebrar alguém.

O critério que uso é direto: se um consumidor existente pode falhar sem alterar uma linha do lado dele, trate como mudança incompatível.

Um exemplo comum é paginação. Trocar page=1&limit=50 por cursor pode ser uma melhoria técnica, principalmente em bases grandes. Mas, para quem consome, isso muda a forma de navegar nos dados. O melhor caminho costuma ser criar um endpoint novo, manter o antigo por uma janela combinada e medir uso real antes de remover.

Depreciação também precisa ter rotina. Marque o campo como depreciado na documentação, avise consumidores conhecidos, acompanhe logs ou métricas de uso e só depois remova. Sem telemetria, você está adivinhando quem ainda depende daquilo.

Erro bom economiza suporte

Quando uma integração falha, a primeira pergunta não deveria ser "alguém consegue olhar o log?". A própria resposta da API deve dar contexto suficiente para o consumidor saber se corrigiu dado, se deve tentar novamente ou se precisa abrir chamado.

Eu gosto de separar mensagem humana de código estável. A mensagem pode melhorar com o tempo. O código é o que sistemas e integrações usam para tomar decisão.

{
  "code": "DOCUMENTO_INVALIDO",
  "message": "O documento informado não possui formato válido.",
  "traceId": "9f4f2b2a7c",
  "details": {
    "field": "documento"
  }
}

Nesse exemplo, DOCUMENTO_INVALIDO é mais importante que o texto. Um app pode mostrar uma mensagem amigável, um integrador pode mapear o erro para uma fila de correção e o suporte pode usar o traceId para achar a chamada.

Também vale padronizar quais erros são temporários. Um 503 de dependência externa pode aceitar retry. Um documento inválido não vai ficar válido só porque o consumidor tentou de novo cinco vezes.

Idempotência evita duplicidade

Operações críticas como pagamento, emissão, cancelamento, baixa de estoque ou protocolo de processo não podem depender da sorte.

Imagine que o cliente chama POST /pagamentos, a API processa o pagamento, mas a conexão cai antes da resposta chegar. Do lado do consumidor, parece timeout. Ele tenta de novo. Se o backend não reconhecer que aquela tentativa já foi processada, você pode criar pagamento duplicado.

A solução prática é pedir uma chave de idempotência para operações sensíveis:

POST /v1/pagamentos
Idempotency-Key: pedido-9842-pagamento-1

O servidor registra a chave, vincula ao resultado da operação e, se a mesma chave chegar novamente, devolve o resultado conhecido em vez de executar tudo de novo. Não é uma proteção cosmética; é o que permite retry seguro.

Rastreabilidade precisa nascer no request

Toda chamada importante deveria nascer com um identificador de correlação. Pode ser X-Correlation-ID, traceparent ou outro padrão adotado pelo time. O nome importa menos que a disciplina de propagar esse valor por logs, filas, eventos e chamadas externas.

Quando alguém pergunta "o que aconteceu com o protocolo 123?", o time precisa reconstruir a história sem fazer arqueologia:

Quem chamou. Quando chamou. Qual validação passou. Qual integração externa demorou. Qual job foi para fila. Qual tentativa falhou. Qual foi o resultado final.

OpenTelemetry ajuda bastante quando você quer padronizar traces, métricas e logs entre serviços. Em times menores, começar com logs estruturados e um correlation ID consistente já melhora muito a investigação.

Auditoria não é log comum

Log operacional responde "por que o sistema se comportou assim?". Auditoria responde "quem fez o quê, quando e com qual consequência de negócio?".

Essas duas coisas não deveriam ser tratadas como a mesma tabela sem critério. Um log de query lenta pode expirar em poucos dias. Um registro de aprovação, cancelamento, emissão ou alteração de status pode precisar ficar disponível por anos, com controle de acesso e retenção diferente.

Um exemplo simples: quando um usuário aprova uma solicitação, o log técnico pode registrar tempo de resposta, rota e status HTTP. A auditoria deveria registrar o usuário, o recurso afetado, o estado anterior, o novo estado, a origem da chamada e o instante do evento.

Misturar tudo dificulta consulta, governança e investigação. Em sistema crítico, auditoria é parte do domínio.

Teste de contrato pega quebra antes do cliente

Teste unitário prova uma parte da implementação. Teste de contrato prova que a promessa pública da API continua de pé.

Para APIs entre serviços, Pact é uma opção conhecida para contract testing orientado a consumidor. Para APIs descritas em OpenAPI, ferramentas como Schemathesis conseguem gerar testes a partir do schema. Prism pode subir mock server a partir do contrato, o que ajuda consumidores a desenvolver antes da API estar completa.

Não precisa começar com uma plataforma enorme. Um primeiro conjunto de cenários já costuma pagar o esforço: payload mínimo válido, erro de validação conhecido, resposta paginada, campo depreciado ainda presente e operação repetida com a mesma chave de idempotência.

O ponto é colocar o contrato no pipeline. Se alguém renomear campo sem perceber, a falha aparece no CI, não na integração do cliente.

Segurança não entra no final

Em API crítica, segurança precisa estar no desenho do contrato. Não basta dizer "tem token". É preciso definir escopos, papéis, vínculo com o recurso, limites de chamada e comportamento esperado para acesso negado.

Uma rota de consulta talvez aceite um escopo amplo. Uma rota de cancelamento precisa validar contexto de negócio, origem, permissão específica e, em alguns casos, proteção contra replay. Integrações sensíveis podem exigir assinatura de payload, allowlist de origem ou mTLS.

O OWASP API Security Top 10 é uma boa referência para revisar riscos comuns: autorização quebrada, exposição excessiva de dados, falta de rate limit, autenticação fraca e consumo inseguro de APIs de terceiros.

Segurança boa é específica. Permissão genérica demais quase sempre vira débito operacional.

Um roteiro pragmático

Se eu fosse começar uma API crítica hoje, eu não tentaria resolver tudo no primeiro commit. Começaria pelo fluxo mais sensível e deixaria algumas decisões explícitas:

  1. Descrever o contrato em OpenAPI, com exemplos reais de sucesso e erro.
  2. Definir códigos de erro estáveis.
  3. Exigir idempotência nas operações que não podem duplicar.
  4. Propagar correlation ID desde a entrada.
  5. Separar log técnico de evento de auditoria.
  6. Colocar lint e teste de contrato no CI.
  7. Medir uso antes de depreciar qualquer coisa.

Depois disso, o time evolui por dor real: mais métricas, mais cenários de contrato, mocks melhores, alertas mais úteis e documentação mais completa.

Conclusão

API crítica precisa ser previsível. Quando dá certo, o consumidor sabe o que recebeu. Quando dá errado, ele entende o motivo. Quando alguém investiga, existe rastro suficiente para reconstruir o caminho.

Versionamento, contrato, rastreabilidade, auditoria, idempotência, testes e segurança não são enfeites de arquitetura. São formas de reduzir surpresa em ambientes onde uma pequena mudança pode afetar muita gente.

Quanto mais importante for o sistema, menos a API pode depender de interpretação informal. O contrato precisa estar escrito, testado, observado e tratado como parte viva do produto.

Posts relacionados