Alterações incompatíveis de API, explicadas

Uma alteração incompatível de API é qualquer alteração ao contrato publicado de uma API que pode fazer um cliente escrito corretamente contra o contrato antigo falhar contra o novo — sem o cliente mudar nada. Essa única frase é toda a teoria; tudo o resto neste artigo são as suas consequências: que alterações contam, que armadilhas famosas parecem inofensivas e não são, como rever uma subida de especificação antes de ser implementada e como as estratégias de versionamento compram tempo aos clientes. Para aplicar isto a dois documentos reais, execute o OpenAPI Diff sobre as especificações antiga e nova e leia os veredictos ao lado desta página.

Um contrato é uma promessa sobre o futuro

Uma especificação de API não é documentação no sentido casual; é uma promessa. Quando uma especificação declara que GET /pets aceita um parâmetro opcional limit e responde 200 com um objeto que contém uma cadeia name, cada autor de clientes constrói exatamente sobre isso: o parâmetro é omitido quando não é preciso, o estado é ramificado, o campo é lido como cadeia. A correção do cliente é julgada contra a promessa, e é por isso que a definição de incompatível é assimétrica — uma alteração é incompatível se um cliente antigo correto pode falhar, mesmo que a maioria dos clientes sobreviva por acaso. Um SDK rigoroso gerado a partir da especificação antiga, a falhar por um tipo alterado, basta: quebrou-se a promessa, não o cliente.

Este quadro resolve a maioria das discussões sobre se uma alteração «conta». Remover um endpoint é incompatível porque a promessa incluía a sua existência. Adicionar um endpoint não é, porque nenhum cliente antigo prometeu a si mesmo nada sobre caminhos que nunca chama. Um campo de resposta opcional novo é seguro porque os analisadores antigos ignoram campos desconhecidos: em qualquer formato sensato, a saída aditiva não pode invalidar um leitor. Uma entrada obrigatória nova, pelo contrário, alcança cada pedido existente e invalida os que a omitem. O teste é sempre o mesmo: reproduza o tráfego válido de ontem contra as regras de amanhã e pergunte o que falha agora.

Dois corolários importam na prática. Primeiro, o contrato é o que é publicado, não o que o servidor faz hoje por acaso: um campo não documentado que os clientes descobriram espiando o tráfego não está prometido, e retirá-lo é rude mas não, estritamente, incompatível — o que é um argumento para documentar o que pretende manter e nada mais. Segundo, o contrato inclui toda a superfície das normas sobre as quais assenta: a semântica de métodos e estados HTTP fixada pela RFC 9110 faz parte da promessa, pelo que redefinir o que significa um 200 ou um 404 é uma alteração de contrato mesmo que o ficheiro de especificação nunca mencione o significado dos estados.

As armadilhas clássicas

A rutura de produção mais comum é a adição de um campo obrigatório. Uma equipa adiciona um campo a um corpo de pedido, marca-o obrigatório porque a nova regra de negócio precisa dele, e publica — após o que cada cliente existente, nenhum dos quais envia o campo, começa a receber 400. As versões seguras são aborrecidas: adicione o campo como opcional com um valor por omissão no servidor, ou exija-o apenas num endpoint novo. A segunda armadilha é a retirada de valores de enumeração: um valor de estado, uma categoria, uma ordem de classificação é retirada porque a nova interface já não a oferece, enquanto os dados armazenados dos clientes, as aplicações móveis em versões antigas e as integrações de parceiros continuam a enviá-la durante anos. Os valores de enumeração custam quase nada a continuar a aceitar; retirar um deve ser tratado como remover um endpoint.

As alterações de tipo são a terceira armadilha, e vêm em disfarces de estreitamento e alargamento. O estreitamento —de cadeia para enumeração, de inteiro para um intervalo menor, de objeto livre para forma fixa— rejeita entradas que antes eram válidas e é incompatível à primeira vista. O alargamento é mais subtil: mudar um campo de resposta de cadeia para cadeia-ou-nulo, ou um id inteiro para um id de cadeia, parece generoso e quebra cada analisador rigoroso e SDK gerado construído para o tipo antigo. No lado da resposta há uma armadilha espelho: um campo que estava sempre presente torna-se opcional. Os clientes leem-no incondicionalmente; a primeira resposta que o omite é um ponteiro nulo no código de outra pessoa.

A quarta armadilha é semântica, e nenhum comparador a consegue apanhar: o campo mantém o nome e o tipo e muda o significado. Um preço que muda em silêncio de cêntimos para dólares, um carimbo de tempo que passa de UTC para hora local, uma lista que estava ordenada e deixa de estar — o diff da especificação está vazio e cada cliente está errado. As ruturas semânticas são a razão de o diff mecânico ser um chão, não um teto, para a revisão: limpa as alterações que não podem quebrar ninguém, para que a atenção humana se concentre nas alterações que simplesmente poderiam.

Rever uma subida de especificação de um fornecedor antes de implementar

Os fornecedores publicam novas versões de especificação constantemente, e «subir o SDK e voltar a implementar» é como as ruturas silenciosas chegam à produção. Uma revisão disciplinada demora quinze minutos. Primeiro, obtenha ambos os documentos —a especificação contra a qual a sua implementação corre hoje na realidade, e a candidata— e compare-os mecanicamente em vez de ler um registo de alterações, porque os registos de alterações são documentos de marketing: listam funcionalidades, não remoções. O OpenAPI Diff produz essa lista localmente, o que importa quando a API é interna e a sua especificação não deve ser colada na ferramenta alojada de um estranho.

Segundo, percorra a lista de incompatíveis como uma lista de verificação contra a sua própria utilização. A maioria das ruturas do fornecedor é teórica para si: um endpoint removido que nunca chama, um campo obrigatório novo num recurso que nunca cria. As entradas perigosas são as que se cruzam com o seu tráfego — é por isso que a revisão precisa do seu inventário de chamadas, não apenas do diff. Terceiro, verifique a superfície não documentada: os estados pelos quais ramifica, as formas de erro que analisa, os cabeçalhos que lê. O mesmo hábito que protege os consumidores de webhooks —fixar e testar o contrato da carga, como em as assinaturas de webhook, explicadas— aplica-se a qualquer contrato que outra pessoa controle.

Por fim, implemente por etapas. Aponte um cliente de pré-produção à nova versão, reproduza pedidos de produção gravados (uma captura saneada, tratada como prescreve o guia HAR) e compare as respostas antes de qualquer utilizador. Se o fornecedor oferecer uma janela de obsolescência ou um pin de versão, aceite-o: a alteração incompatível mais barata é a que adota ao seu ritmo, quando os seus clientes estão prontos, e não a do dia de publicação do fornecedor.

Estratégias de versionamento: URL, cabeçalho, ou nunca quebrar

A resposta clássica à alteração incompatível é um identificador de versão, e a discussão clássica é onde o pôr. O versionamento por URL (/v1/, /v2/) é explícito, amigável à cache, trivialmente encaminhável e visível em cada linha de registo; o seu custo é que versiona toda a API de uma vez, pelo que um endpoint incompatível arrasta cada cliente por uma migração. O versionamento por cabeçalho —um cabeçalho de versão ou um esquema de negociação de conteúdo datado— mantém os URL limpos e pode versionar recursos individuais, ao preço da invisibilidade: a versão desaparece dos registos, das ligações e das barras do navegador, e depurar torna-se arqueologia. Nenhum está errado; a indústria simplesmente paga o custo do URL de melhor grado porque a operacionalidade vence a elegância às 3 da manhã.

A estratégia mais profunda é precisar de versões raramente. A evolução apenas aditiva —nunca remover, nunca exigir, nunca mudar tipos; apenas adicionar coisas opcionais— mantém uma única versão viva indefinidamente, que é como de facto operam as API públicas mais longevas. Exige disciplina desde o início: tolerância generosa na entrada, ids tipados como cadeias desde o primeiro dia (para que o intervalo possa crescer), enumerações desenhadas para serem estendidas e um processo de obsolescência que avisa muito antes de retirar seja o que for. O versionamento semântico, codificado em semver.org, fornece a camada de sinalização que a própria especificação da OpenAPI Initiative deixa deliberadamente aos editores: uma subida maior anuncia uma rutura, e os clientes podem fixar-se contra ela.

Seja qual for a estratégia escolhida, só funciona como contrato social apoiado por mecânica: as versões têm de correr realmente em paralelo durante uma janela declarada, as obsolescências têm de ser anunciadas onde os autores de clientes as vejam (registo de alterações, cabeçalhos de resposta, aviso direto aos grandes consumidores) e a utilização da versão antiga tem de ser medida para que «já ninguém a usa» seja um facto, não uma esperança. Uma versão que é publicada mas não pode operar em paralelo não é uma estratégia; é uma mudança de nome da rutura.

Onde o comparador se encaixa

Um comparador mecânico ganha o seu lugar em dois momentos. Na revisão, converte «quebrou-se alguma coisa?» de uma tarde de leitura lado a lado numa lista que se pode riscar — e, igualmente útil, prova um negativo: quando uma versão afirma ser apenas aditiva, uma lista de incompatíveis vazia é evidência, não sensações. Na integração contínua, a mesma comparação pode correr em cada pedido de alteração que toque uma especificação, falhando a construção quando uma alteração incompatível chega sem a subida de versão ou a aprovação que a política do projeto exige. A tabela de classificação é a política; a ferramenta simplesmente recusa deixar que seja esquecida à hora da entrega.

Os seus limites definem o resto do fluxo de trabalho. O comparador lê apenas o contrato declarado: não consegue ver o comportamento, o desempenho, a semântica nem as peculiaridades não documentadas de que os clientes dependem, e resume os esquemas patologicamente profundos em vez de os expandir para sempre. Por isso a porta completa é uma canalização, não uma página: diff mecânico para o contrato, testes de contrato que reproduzem chamadas reais de clientes para o comportamento, implementação por etapas para a semântica e monitorização para o dia em que os três falharem alguma coisa. Usado assim, o diff é a camada mais barata das quatro — quinze segundos que eliminam a classe de falha mais embaraçosa, aquela em que a rutura estava escrita na especificação o tempo todo e ninguém comparou os documentos.

É esse o hábito que este artigo defende na realidade. As especificações são baratas de guardar e ainda mais baratas de comparar; os incidentes não. Guarde cada versão publicada de cada contrato que possua ou consuma, compare cada candidata com a versão em produção antes de ser publicada, e trate a lista de incompatíveis como a agenda real da versão. As equipas que fazem isto deixam de discutir se uma alteração «devia ter» quebrado alguém — têm a lista, datada e arquivada, de antes de ser publicada.

Fontes e leituras adicionais

Perguntas frequentes

Adicionar um campo obrigatório é sempre incompatível?

Sim, em qualquer endpoint que os clientes existentes já chamem: os pedidos que ontem eram válidos hoje são rejeitados, que é a definição de incompatível. As saídas padrão são adicionar o campo como opcional com um valor por omissão no servidor, exigi-lo apenas num endpoint ou versão novos, ou anunciar uma janela de obsolescência durante a qual o campo é aceite mas com aviso antes de se tornar obrigatório.

Versionamento por URL ou por cabeçalho — qual é melhor?

Para a maioria das equipas, o de URL: é visível em registos e ligações, fácil de encaminhar e colocar em cache, e honesto sobre o que é. O de cabeçalho convém a plataformas que precisam de granularidade por recurso e já têm uma observabilidade forte. Ambos vencem a pior opção, que é a alteração incompatível sem versão. A melhor estratégia de todas é a evolução apenas aditiva, que torna o versionamento um evento raro em vez de um imposto rotineiro.

Uma ferramenta pode apanhar cada alteração incompatível?

Não. Um comparador apanha tudo o que a especificação declara: remoções, entradas obrigatórias novas, alterações de tipo, retiradas de enumerações. Não consegue apanhar alterações semânticas —mesmo campo, novo significado— nem o comportamento que a especificação nunca declarou. É por isso que o diff mecânico pertence a uma canalização com testes de contrato e implementação por etapas, não a um pedestal próprio.