Se você não está muito familiarizado com APIs, pode estar se perguntando… por que toda essa confusão em torno do versionamento de API’s?
Se você já foi impactado por mudanças em APIs, provavelmente é você quem está preocupado. Se você é o responsável por manter uma API, também pode estar lidando com perguntas desafiadoras como estas:
# Esta é a versão 2 apenas dos produtos ou de toda a API? /v2/products # O que catalisou a mudança entre v1 e v2? Como elas são diferentes? /v1/products /v2/products
Essas perguntas sobre versionamento não são fáceis de responder. Nem sempre é claro a que a v1 ou v2 se refere. E não devemos simplesmente criar uma segunda versão de um endpoint quando a primeira parecer não ser mais suficiente.
Há razões claras pelas quais sua API precisa ter versionamento, e existem estratégias claras para navegar efetivamente pelas mudanças na API.
No entanto, descobri que a maioria dos desenvolvedores – incluindo eu mesmo, até aprender algumas lições da maneira mais difícil – não conhece essas razões e estratégias.
Este artigo busca destacar essas razões para o versionamento e estratégias para realizá-lo. Vamos assumir um contexto de API REST, já que é um padrão para muitas API’s, e focar no aspecto de versionamento.
O que é Versionamento?
Devemos começar esclarecendo o que significa o termo “versionamento de API”. Aqui está nossa definição:
O versionamento de API’s é a prática de gerenciar de maneira transparente as mudanças na sua API.
Versionamento é comunicação eficaz sobre mudanças em sua API, para que os consumidores saibam o que esperar dela. Você está fornecendo dados ao público de alguma forma e precisa comunicar quando altera a maneira como esses dados são entregues.
Isso se resume, na prática, a gerenciar contratos de dados e mudanças disruptivas. O primeiro é o bloco de construção principal da sua API e o último revela por que o versionamento é necessário.
Contratos de Dados
Uma API é uma Application Programming Interface, e uma interface é uma fronteira compartilhada para troca de informações. O contrato de dados é o coração dessa interface.
Um contrato de dados é um acordo sobre a estrutura e o conteúdo geral dos dados de request e/ou response.
Para ilustrar um contrato de dados, aqui está um corpo de response JSON básico:
{
"data": [
{
"id": 1,
"name": "Produto 1"
},
{
"id": 2,
"name": "Produto 2"
}
]
}
Esse é um objeto com uma propriedade data
que é uma lista de produtos, cada um com uma propriedade id
e name
. Mas a propriedade data
poderia facilmente ter sido chamada de body
, e a propriedade id
em cada produto poderia ser um GUID em vez de um número inteiro. Se um único produto estivesse sendo retornado, data
poderia ser um objeto em vez de uma lista.
Essas mudanças aparentemente sutis teriam criado um acordo diferente, um contrato diferente, em relação ao “formato” dos dados. O formato dos dados poderia se aplicar a nomes de propriedades, tipos de dados ou até mesmo ao formato esperado (JSON vs. XML).
Por que o Versionamento é Necessário?
Com APIs, algo tão simples quanto mudar o nome de uma propriedade de productId
para productID
pode causar problemas para os consumidores. Isso aconteceu com nossa equipe na semana passada.
Felizmente, tínhamos testes para capturar mudanças no contrato da API. No entanto, não deveríamos precisar desses testes, pois os responsáveis pela API deveriam saber que isso seria uma mudança disruptiva.
Mudanças Disruptivas
Essa foi uma mudança disruptiva no contrato de dados acordado, porque a alteração deles nos forçou a mudar nossa aplicação também.
O que constitui uma “mudança disruptiva” em um endpoint de API? Qualquer mudança no seu contrato de API que force o consumidor a fazer uma alteração também.
As mudanças disruptivas geralmente se encaixam nas seguintes categorias:
- Mudança no formato de request/response (por exemplo, de XML para JSON)
- Mudança no nome de uma propriedade (por exemplo, de
name
paraproductName
) ou no tipo de dado de uma propriedade (por exemplo, de um número inteiro para um número de ponto flutuante) - Adição de um campo obrigatório na request (por exemplo, um novo header obrigatório ou uma propriedade no corpo da request)
- Remoção de uma propriedade na response (por exemplo, removendo
description
de um produto)
Gerenciamento de Mudanças na API
Nunca é sábio ou gentil forçar os consumidores de uma API a fazer uma mudança. Se você precisar fazer uma mudança disruptiva, é para isso que o versionamento serve, e vamos cobrir as formas mais eficazes de versionar sua aplicação e seus endpoints.
Mas primeiro, vamos discutir brevemente como evitar mudanças disruptivas desde o início. Podemos chamar isso de gerenciamento de mudanças de API.
O gerenciamento eficaz de mudanças no contexto de uma API é resumido pelos seguintes princípios:
- Continuar a dar suporte a propriedades/endpoints existentes
- Adicionar novas propriedades/endpoints em vez de alterar os existentes
- Descontinuar de forma planejada propriedades/endpoints obsoletos
Aqui está um exemplo que demonstra todos os três princípios no contexto da response para request de dados do usuário:
{
"data": {
"id": 1,
"name": "Carlos Ray Norris", // propriedade original
"firstName": "Carlos", // nova propriedade
"lastName": "Norris", // nova propriedade
"alias": "Chuck", // propriedade obsoleta
"aliases": ["Chuck", "Walker"] // nova propriedade
},
"meta": {
"fieldNotes": [
{
"field": "alias",
"note": "Desativando em [data futura]. Por favor, use `aliases`."
}
]
}
}
Nesse exemplo, name
era uma propriedade original. Os campos firstName
e lastName
estão sendo implementados para fornecer uma opção mais granular, caso o consumidor queira exibir “Sr. Norris” com interpolação de string, sem precisar analisar o campo name
. No entanto, a propriedade name
continuará sendo suportada.
alias
, por outro lado, será descontinuada em favor do array aliases
—porque Chuck tem tantos apelidos—e há uma nota na response indicando o cronograma de desativação.
Como Versionar uma API?
Esses princípios ajudarão bastante na navegação de mudanças em sua API sem a necessidade de lançar uma nova versão. No entanto, às vezes isso é inevitável, e se você precisar de um novo data contract, precisará de uma nova versão do seu endpoint. Portanto, você precisará comunicar isso ao público de alguma forma.
A título de nota, observe que não estamos falando sobre a versão da base de código subjacente. Assim, se você estiver usando versionamento semântico para sua aplicação que também suporta uma API pública, provavelmente você vai querer separar esses sistemas de versionamento.
Como você cria uma nova versão da sua API? Quais são os diferentes métodos para fazê-lo? Você precisará determinar qual tipo de estratégia de versionamento deseja adotar em geral e, em seguida, ao desenvolver e manter sua API, precisará determinar o escopo de cada mudança de versão.
Escopo
Vamos abordar o escopo primeiro. Como exploramos acima, às vezes os data contracts serão comprometidos por uma mudança disruptiva, e isso significa que precisaremos fornecer uma nova versão do data contract. Isso pode significar uma nova versão de um endpoint ou pode significar uma mudança em um escopo de aplicação mais global.
Podemos pensar em níveis de mudança de escopo dentro de uma analogia de árvore:
- Folha (leaf) – Uma mudança em um endpoint isolado, sem relação com outros endpoints.
- Ramo (branch) – Uma mudança em um grupo de endpoints ou em um recurso acessado através de vários endpoints.
- Tronco (trunk) – Uma mudança em nível de aplicação, que justifica uma mudança de versão na maioria ou em todos os endpoints.
- Raiz (Root) – Uma mudança que afeta o acesso a todos os recursos da API de todas as versões.
Como você pode ver, ao passar de folha para raiz, as mudanças se tornam progressivamente mais impactantes e globais em escopo.
O escopo da folha pode frequentemente ser tratado por meio de uma gestão eficaz de mudanças na API. Caso contrário, basta criar um novo endpoint com o novo data contract de recurso.
Um ramo é um pouco mais complicado, dependendo de quantos endpoints são afetados pela mudança do data contract no recurso em questão. Se as mudanças forem relativamente restritas a um grupo claro de endpoints relacionados, você pode potencialmente navegar por isso introduzindo um novo nome para o recurso e atualizando sua documentação de acordo.
# variants, que possuem uma mudança disruptiva é acessada em múltiplas rotas /variants /products/:id/variants # nós introduzimos product-variants em vez disso /product-variants /products/:id/product-variants
Um tronco refere-se a mudanças em nível de aplicação que são frequentemente resultado de uma mudança em uma das seguintes categorias:
- Formato (por exemplo, de XML para JSON)
- Especificação (por exemplo, de uma interna para JSON API ou Open API)
- Headers obrigatórios (por exemplo, para autenticação/autorização)
Essas mudanças necessitarão uma alteração na versão geral da sua API, então você deve planejar cuidadosamente e executar a transição de forma adequada.
Uma mudança de raiz obrigará você a ir um passo além para garantir que todos os consumidores de todas as versões da sua API estejam cientes da mudança.
Tipos de Versionamento de API
À medida que nos voltamos para diferentes tipos de versionamento de API, queremos usar essas percepções sobre os vários escopos de mudanças na API para avaliar os tipos. Cada abordagem tem seu próprio conjunto de pontos fortes e fracos em relação a mudanças com base em seu escopo.
Existem vários métodos para gerenciar a versão da sua API. O versionamento por Path de URI é o mais comum.
Path de URI
http://www.example.com/api/v1/products http://api.example.com/v1/products
Essa estratégia envolve colocar o número da versão no Path do URI e é frequentemente feita com o prefixo “v”. Mais frequentemente do que não, os projetistas de API usam isso para se referir à versão de sua aplicação (ou seja, “tronco”) em vez da versão do endpoint (ou seja, “folha” ou “ramo”), mas isso nem sempre é uma suposição segura.
O versionamento por Path de URI implica lançamentos orquestrados de versões de aplicações que requererão uma das duas abordagens: manter uma versão enquanto desenvolve uma nova ou forçar os consumidores a esperar por novos recursos até que a nova versão seja lançada. Isso também significa que você precisaria levar adiante qualquer endpoint que não tenha mudado de versão para versão. No entanto, para APIs com volatilidade relativamente baixa, ainda é uma opção razoável.
Você provavelmente não gostaria de relacionar seu número de versão ao do endpoint ou recurso, porque isso facilmente resultaria em algo como uma v4 de produtos, mas uma v1 de variantes, o que seria bastante confuso.
Query Params
http://www.example.com/api/products?version=1
Esse tipo de versionamento adiciona um parâmetro de consulta à requisição que indica a versão. Muito flexível em termos de requisitar a versão do recurso que você gostaria no nível “folha”, mas não possui noção da versão geral da API e se presta aos mesmos problemas de desincronização mencionados no comentário acima sobre o versionamento em nível de endpoint do Path de URI.
Header
Accept: version=1.0
A abordagem de header é aquela que oferece mais granularidade ao fornecer a versão requisitada de qualquer recurso dado.
No entanto, está oculta no objeto de requisição e não é tão transparente quanto a opção de Path de URI. Também ainda é difícil dizer se 1.0 se refere à versão do endpoint ou da própria API.
Integração de Tipos
Cada uma dessas abordagens parece ter a fraqueza de favorecer um escopo “folha” ou “tronco”, mas não suportar ambos.
Se você precisa manter a versão geral da API e também fornecer suporte para várias versões de recursos, considere uma combinação dos tipos Path de URI e Query Params, ou uma abordagem de header mais avançada.
# Combinação de Path de URI e query params http://api.example.com/v1/products?version=1 http://api.example.com/v1/products?version=2 # Headers estendidos, para http://api.example.com/products Accept: api-version=1; resource-version=1 Accept: api-version=1; resource-version=2
Conclusão
Cobrimos muitos aspectos aqui, então vamos recapitular:
- O versionamento de API é a prática de gerenciar mudanças em sua API de forma transparente.
- Gerenciar uma API se resume a definir e evoluir data contracts e lidar com mudanças disruptivas.
- A maneira mais eficaz de evoluir sua API sem mudanças disruptivas é seguir princípios eficazes de gestão de mudanças na API.
- Para a maioria das APIs, o versionamento no Path de URI é a solução mais direta.
- Para APIs mais complexas ou voláteis, você pode gerenciar vários escopos de mudanças empregando uma integração das abordagens de Path de URI e query params.
Embora esses princípios devam fornecer uma direção clara sobre como gerenciar efetivamente as mudanças em suas APIs, evoluir uma API é potencialmente mais uma arte do que uma ciência. Isso requer reflexão e previsão para criar e manter uma API confiável.