Design de APIs que sobrevivem: versionamento, contratos e retrocompatibilidade
Uma API não é código, é uma promessa. No momento em que o primeiro cliente integra com o seu endpoint, você assinou um contrato: "enquanto você chamar assim, vou responder assado". O software por trás pode mudar todo dia; o contrato, não. A diferença entre uma API que dura anos e uma que gera incidentes a cada release está inteiramente na disciplina com que essa promessa é gerenciada.
Este guia é sobre a parte difícil de APIs: como fazê-las evoluir sem quebrar quem já depende delas. Versionamento, contratos, mudanças seguras e deprecação com respeito.
O contrato é o produto
Quando falamos de uma API pública ou interna consumida por vários times, o consumidor não enxerga a sua arquitetura, seu banco ou sua linguagem. Ele enxerga apenas o contrato: os endpoints, os formatos de requisição e resposta, os códigos de status, os erros. Esse contrato é a superfície pública, e tudo o que está nela é território sagrado.
A consequência prática é dura: você pode refatorar o mundo inteiro por trás, mas não pode renomear um campo de resposta sem consequências. Por isso, o primeiro princípio de um bom design de API é separar o que é interface do que é implementação, e proteger a interface com contratos explícitos.
Mudanças breaking x não-breaking
Toda mudança em uma API cai em uma de duas categorias, e saber diferenciá-las é o coração do assunto.
Uma mudança não-breaking (retrocompatível) é aquela que um cliente antigo continua funcionando sem alteração:
- Adicionar um endpoint novo.
- Adicionar um campo opcional na requisição.
- Adicionar um campo novo na resposta (clientes bem-comportados ignoram o que não conhecem).
- Adicionar um novo valor a um enum de saída, com cuidado.
Uma mudança breaking quebra clientes existentes:
- Remover ou renomear um campo.
- Mudar o tipo de um campo (string vira número).
- Tornar obrigatório um campo que era opcional.
- Mudar semântica, código de status ou o formato de erro.
- Remover um endpoint ou apertar validações.
| Tipo de mudança | Exemplo | Quebra clientes? | Exige nova versão? |
|---|---|---|---|
| Adicionar endpoint | POST /faturas |
Não | Não |
| Adicionar campo opcional na resposta | + status_pagamento |
Não | Não |
| Tornar campo obrigatório | cpf passa a ser required |
Sim | Sim |
| Renomear campo | nome → nome_completo |
Sim | Sim |
| Mudar tipo | valor: "10.00" → 10.0 |
Sim | Sim |
| Remover endpoint | DELETE /v1/legado |
Sim | Sim |
A regra de ouro: adicionar é barato, mudar e remover é caro. Projete pensando em adição.
Estratégias de versionamento
Quando uma mudança breaking é inevitável, você precisa versionar. Há três abordagens dominantes, com trade-offs reais.
Versão na URL
A mais comum e explícita: https://api.exemplo.com/v1/pedidos. A versão fica visível, é trivial rotear e cachear, e qualquer pessoa entende num relance.
- Prós: simples, visível, fácil de testar no navegador, roteamento direto.
- Contras: viola a pureza REST (a URL deveria identificar o recurso, não a versão) e tende a proliferar versões inteiras da API.
Versão no header
A versão vai em um cabeçalho, como Accept: application/vnd.exemplo.v2+json ou um API-Version: 2026-07-01. A URL permanece limpa e estável.
- Prós: URLs estáveis, versionamento mais granular, alinhado com content negotiation.
- Contras: menos visível, mais difícil de testar manualmente, exige clientes disciplinados.
Versão por data (rolling)
Popularizada por grandes plataformas: o cliente fixa uma data (API-Version: 2026-05-01) e o servidor aplica transformações para manter aquele comportamento congelado. Novas versões são datas, não números.
- Prós: evolução contínua, o cliente escolhe quando migrar, sem saltos de versão inteira.
- Contras: complexidade alta no servidor, que precisa manter camadas de compatibilidade.
Para a maioria dos produtos, versão na URL com v1, v2 é o padrão pragmático. Reserve o versionamento por data para APIs de grande escala e público amplo.
Contratos com OpenAPI
Contrato que vive só na cabeça do time não é contrato. A forma moderna de tornar a promessa executável é a especificação OpenAPI (antiga Swagger): um documento que descreve cada endpoint, parâmetro, schema de request/response e erro.
Um contrato formal habilita coisas que valem ouro:
- Documentação sempre sincronizada, gerada da fonte da verdade.
- Geração de SDKs e de tipos para clientes, em qualquer linguagem.
- Validação automática de requests e responses contra o schema.
- Mock servers para o front começar antes do back terminar.
O padrão ideal é contract-first: escreve-se o contrato antes do código, ele é revisado como parte do design, e o servidor é validado contra ele. O contrato deixa de ser documentação e vira o eixo do desenvolvimento.
# trecho de um contrato OpenAPI
paths:
/v1/pedidos/{id}:
get:
summary: Busca um pedido
parameters:
- name: id
in: path
required: true
schema: { type: string, format: uuid }
responses:
'200':
description: Pedido encontrado
content:
application/json:
schema:
$ref: '#/components/schemas/Pedido'
'404': { description: Pedido não encontrado }
components:
schemas:
Pedido:
type: object
required: [id, status, total]
properties:
id: { type: string, format: uuid }
status: { type: string, enum: [aberto, pago, cancelado] }
total: { type: number }
Deprecação com respeito
Toda versão morre um dia, mas matar sem aviso é como derrubar uma ponte com carros em cima. Um processo de deprecação responsável tem etapas claras:
- Anuncie com antecedência generosa, por changelog, e-mail e documentação.
- Sinalize na resposta: use o header
Deprecation: trueeSunset: <data>para que clientes automatizados detectem o fim programado. - Monitore o uso residual: meça quantos clientes ainda batem na versão antiga antes de desligá-la.
- Ofereça um caminho de migração: guia claro do que mudou e como adaptar, idealmente com exemplos lado a lado.
- Desligue de fato só depois de o uso cair a um nível aceitável, e comunique o desligamento efetivo.
A cortesia técnica aqui é reputação: APIs que quebram sem aviso perdem a confiança dos integradores rápido, e confiança é o ativo mais caro de reconstruir.
Testes de contrato
Como garantir que o servidor nunca viole a promessa por acidente, num deploy comum de sexta à tarde? Com testes de contrato. Diferente de testes de integração tradicionais, eles verificam especificamente que a resposta continua conforme o schema acordado.
- Provider tests: validam que a API respeita seu próprio contrato OpenAPI a cada build.
- Consumer-driven contracts (ex.: Pact): o consumidor descreve o que espera, e o provedor é testado contra essas expectativas, pegando quebras antes de chegarem em produção.
Rodando isso no CI, uma mudança breaking acidental falha o pipeline em vez de gerar um incidente. O contrato deixa de ser confiança e passa a ser garantia automatizada.
Boas práticas que envelhecem bem
Alguns hábitos de design tornam a evolução muito menos dolorosa:
- Prefira campos a posições: objetos nomeados evoluem melhor que arrays posicionais.
- Erros estruturados e consistentes: um formato de erro padrão (código, mensagem, detalhes) em toda a API.
- Paginação, filtros e ordenação pensados desde o v1, porque adicioná-los depois costuma ser breaking.
- Idempotência em operações de escrita, com chave de idempotência, para clientes poderem repetir com segurança.
- Tolerância do cliente: ensine consumidores a ignorar campos desconhecidos: isso torna futuras adições não-breaking de fato.
APIs são infraestrutura de longo prazo
Projetar uma API é assumir responsabilidade por um contrato que pode durar mais que o time que o escreveu. Versionamento claro, contratos formais em OpenAPI, disciplina entre mudanças breaking e não-breaking, deprecação comunicada e testes de contrato no CI são o que separa uma plataforma confiável de uma fonte crônica de incidentes. Não é sobre nunca mudar: é sobre mudar sem trair quem confiou em você.
Na ConstructorTI, projetamos e evoluímos APIs pensando no ciclo de vida inteiro: contract-first com OpenAPI, versionamento pragmático, testes de contrato automatizados e estratégias de deprecação que preservam a confiança dos integradores. Se a sua API está difícil de evoluir, quebrando clientes a cada release ou sem contrato formal, fale com o nosso time de engenharia e saia da conversa com um plano de versionamento e retrocompatibilidade sob medida.