Artigo original: How to Version a REST API

Se você não está muito familiarizado com APIs, pode estar se perguntando... por que tanta conversa sobre o versionamento de API?

Se você já teve de fazer alterações na API, provavelmente é você quem está se conversando sobre o assunto. Se você mantém uma API, também pode estar tentando responder a perguntas desafiadoras como estas:

# Esta é a versão 2 apenas de 'produtos' ou da API inteira?
/v2/produtos

# O que gerou a mudança de v1 para v2? Qual é a diferença entre elas?
/v1/produtos
/v2/produtos

Essas perguntas sobre versionamento não são fáceis de responder. Nem sempre está claro a que v1 ou v2 se referem. Não devemos simplesmente fazer uma segunda versão de um endpoint quando a primeira já não parece ser suficiente.

Há razões claras pelas quais sua API precisa ter versionamento. Também existem estratégias claras sobre como navegar efetivamente pelas alterações da API.

No entanto, descobri que a maioria dos desenvolvedores – incluindo eu mesmo até ter aprendido algumas lições da maneira mais difícil – não está ciente dessas razões e estratégias.

Este artigo procura mostrar as razões para o versionamento e as estratégias para realizá-lo. Vamos assumir um contexto de API REST – pois é um padrão para muitas APIs – e nos concentrar no aspecto do versionamento.

O que é o versionamento?

Devemos começar com a definição de nível sobre o que significa o termo "versionamento de API". Aqui está a nossa definição de trabalho:

O versionamento de APIs é a prática de gerenciar de maneira transparente as alterações em sua API.

O versionamento é uma comunicação eficaz em torno de alterações em sua API, para que quem as consome saiba o que esperar dela. Você está entregando dados para o público de algum modo e precisa comunicar quando muda a maneira como os dados são entregues.

O resumo disso é que, no mínimo, é preciso gerenciar contratos de dados e quebrar mudanças. O primeiro é o bloco de construção principal da sua API e o segundo revela o motivo de o versionamento ser necessário.

Contratos de dados

Uma API é uma Interface de Programação de Aplicações. Uma interface é um limite compartilhado para a troca de informações. O contrato de dados é o coração dessa interface.

Um contrato de dados é um acordo sobre o modo e o conteúdo geral dos dados de solicitação e/ou resposta.

Para ilustrar um contrato de dados, aqui está um corpo de resposta JSON básico:

{
  "dados": [
    {
      "id": 1,
      "nome": "Produto 1"
    },
    {
      "id": 2,
      "nome": "Produto 2"
    }
  ]
}

O JSON é um objeto com uma propriedade dados, que é um array (lista) de produtos, cada um desses produtos com uma propriedade id e uma propriedade nome. A propriedade dados, porém, também poderia ter sido facilmente chamada de body. A propriedade id de cada produto, por sua vez, poderia ter sido um GUID em vez de um número inteiro. Se um único produto estivesse sendo retornado, dados poderia ser um objeto em vez de um array.

Essas mudanças aparentemente sutis teriam criado um acordo diferente, um contrato diferente, em relação ao "formato" no qual os dados são apresentados. O formato dos dados pode ser aplicado a nomes de propriedades, tipos de dados ou até mesmo ao formato esperado (JSON ou XML).

Por que o versionamento é necessário?

Com as APIs, algo tão simples como alterar o nome de uma propriedade de IdDoProduto para IDDoProduto pode causar problemas para quem consome a API. Isso foi exatamente o que aconteceu com a nossa equipe na semana passada.

Felizmente, fizemos testes para detectar alterações no contrato da API. No entanto, não deveríamos ter precisado desses testes, porque os mantenedores da API deveriam saber que essa seria uma mudança que causaria problemas.

Mudanças problemáticas

Essa foi uma mudança problemática no contrato de dados acordado, pois a mudança deles nos obrigou a também mudar nossa aplicação.

O que constitui uma "mudança problemática" em um endpoint de API?

As alterações de quebra se encaixam principalmente nas seguintes categorias:

  1. Alterar o formato de solicitação/resposta (por exemplo, de XML para JSON)
  2. Alterar um nome de propriedade (por exemplo, de nome para nomeDoProduto) ou tipo de dados em uma propriedade (por exemplo, de número inteiro para número de ponto flutuante)
  3. Adicionar um campo obrigatório na solicitação (por exemplo, um novo cabeçalho ou propriedade obrigatório em um corpo de solicitação)
  4. Remover uma propriedade na resposta (por exemplo, remover a descricao de um produto)

Gerenciamento de alterações da API

Nunca é sábio ou gentil forçar os consumidores de uma API a fazer uma mudança. Se você precisar fazer uma alteração problemática, use o versionamento para isso. Abordaremos as maneiras mais eficazes de fazer a versão da sua aplicação e dos endpoints.

Primeiramente, vamos discutir brevemente como evitar mudanças problemáticas. Poderíamos chamar isso de gerenciamento de alterações da API.

O gerenciamento eficaz de alterações no contexto de uma API é resumido pelos seguintes princípios:

  • Manter o suporte a propriedades/endpoints existentes
  • Adicionar novas propriedades/endpoints em vez de alterar os existentes
  • Tornar obsoletos propriedades/endpoints com muito cuidado

Aqui está um exemplo que demonstra todos esses três princípios no contexto da resposta para solicitar dados do usuário:

{
  "dados": {
    "id": 1,
    "nome": "Carlos Ray Norris",       // propriedade original
    "nomeInicial": "Carlos",           // propriedade nova
    "sobreNome": "Norris",             // propriedade nova
    "apelido": "Chuck",                // propriedade obsoleta
    "apelidos": ["Chuck", "Walker"]   // propriedade nova
  },
  "meta": {
    "notasDosCampos": [
      {
        "campo": "apelido",
        "nota": "Será tornado obsoleto em [data futura]. Utilize apelidos em seu lugar."
      }
    ]
  }
}

Nesse exemplo, nome era uma propriedade original. Os campos nomeInicial e sobrenome estão sendo implementados para fornecer uma opção mais granular, caso quem consuma a API queira exibir "Sr. Norris" com alguma interpolação de strings, mas sem ter que analisar o campo nome. No entanto, a propriedade nome continuará a ser suportada.

apelido, por outro lado, será preterido em favor do array apelidos – já que Chuck tem muitos apelidos – há uma nota na resposta para indicar o tempo até que o campo se torne obsoleto.

Como fazer a versão de uma API?

Esses princípios farão uma grande diferença na navegação pelas alterações em sua API sem a necessidade de lançar uma nova versão. No entanto (e, às vezes, isso é inevitável), se você precisar de um novo contrato de dados, precisará de uma nova versão do seu endpoint. Então, você precisará comunicar isso ao público de alguma maneira.

Como um aparte, observe que não estamos falando sobre a versão da base de código subjacente. Portanto, se você estiver usando o versionamento semântico (link em inglês) para sua aplicação que também oferece suporte a uma API pública, provavelmente desejará separar esses sistemas de versionamento.

Como criar uma outra versão da sua API? Quais são os diferentes métodos para fazê-lo? Você precisará determinar que tipo de estratégia de versionamento deseja adotar em geral e, à medida que desenvolve e mantém sua API, precisará determinar o escopo de cada alteração de versão.

Escopo

Vamos abordar o escopo primeiro. Como exploramos acima, às vezes, os contratos de dados serão comprometidos por uma alteração problemática. Isso significa que precisaremos fornecer uma nova versão do contrato de dados. Isso pode representar uma nova versão de um endpoint ou uma alteração em um escopo mais global da aplicação.

Podemos pensar em níveis de mudança de escopo dentro de uma analogia de árvore:

  • Folha – uma alteração em um endpoint isolado, sem relação com outros endpoints
  • Galho – uma alteração em um grupo de endpoints ou em um recurso acessado por vários endpoints
  • Tronco – uma alteração no nível da aplicação, causando uma alteração de versão na maioria ou em todos endpoints
  • Raiz – uma alteração que afeta o acesso a todos os recursos da API de todas as versões

Como você pode ver, passando da folha até a raiz, as mudanças se tornam progressivamente mais impactantes e de alcance global.

O escopo de folha geralmente pode ser manipulado por meio do gerenciamento eficaz de alterações de API. Caso contrário, basta criar um outro endpoint com o novo contrato de dados de recursos.

Um galho é um pouco mais complicado, dependendo de quantos endpoints são afetados pela alteração do contrato de dados no recurso em questão. Se as alterações estiverem relativamente confinadas a um grupo claro de endpoints relacionados, você poderá navegar por isso introduzindo um novo nome para o recurso e atualizando seus documentos de acordo.

# variantes, que possui uma mudança problemática, é acessada por diversas rotas
/variantes
/produtos/:id/variantes

# apresentamos variantes-do-produto em seu lugar
/variantes-do-produto
/produtos/:id/variantes-do-produto

Um tronco refere-se a alterações no nível da aplicação que, geralmente, são resultado de uma alteração em uma das seguintes categorias:

  • Formato (por exemplo, de XML para JSON)
  • Especificação (por exemplo, de uma especificação interna para uma da JSON API ou da Open API)
  • Cabeçalhos necessários (por exemplo, para autenticação/autorização)

Essas categorias exigirão uma alteração na versão geral da API. Portanto, você deve planejar cuidadosamente e executar bem a transição.

Uma alteração de raiz forçará você a dar um passo adiante para garantir que todos os consumidores de todas as versões de sua API estejam cientes da alteração.

Tipos de versionamento de API

À medida que nos voltamos para diferentes tipos de versionamento de API, desejaremos usar esses insights em escopos variados de alterações de API para avaliar os tipos. Cada abordagem tem seu próprio conjunto de pontos fortes e fracos para lidar com as mudanças com base em seu escopo.

Existem vários métodos para gerenciar a versão da API. O versionamento do caminho do URI (Uniform Resource Identifier, ou, em português, identificador uniforme de recurso) é o mais comum.

Caminho do URI

http://www.exemplo.com/api/v1/produtos
http://api.exemplo.com/v1/produtos

Essa estratégia envolve colocar o número da versão no caminho do URI e geralmente é feita com o prefixo "v". Na maioria das vezes, os criadores de APIs o usam para se referir à versão da aplicação (ou seja, o "tronco") em vez de a versão do endpoint (ou seja, da "folha" ou do "galho"), mas essa nem sempre é uma suposição segura.

O versionamento do caminho de URI implica versões orquestradas de versões de aplicação que exigirão uma de 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 transferir quaisquer endpoints não alterados de versão para versão. No entanto, para APIs com volatilidade relativamente baixa, ainda é uma opção decente.

Você, provavelmente, não gostaria de relacionar seu número de versão com o do endpoint ou recurso, pois isso resultaria facilmente em algo como uma v4 de produtos, mas uma v1 de variantes, o que seria bastante confuso.

Parâmetros de consulta

http://www.exemplo.com/api/produtos?versao=1

Esse tipo de versionamento adiciona um parâmetro de consulta (do inglês, query) à solicitação que indica a versão. Muito flexível em termos de solicitação da versão do recurso que você gostaria no nível de "folha", mas não tem noção da versão geral da API e se presta aos mesmos problemas de assincronia mencionados no comentário acima sobre o versionamento em nível de endpoint do caminho do URI.

Cabeçalho

Accept: versao=1.0

A abordagem de cabeçalho é aquela que fornece mais granularidade no fornecimento da versão solicitada de qualquer recurso específico.

No entanto, ela fica oculta dentro do objeto de solicitação e não é tão transparente quanto a opção do caminho do URI. Também é 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 de "folha" ou de "tronco", mas sem dar suporte ao outro tipo.

Se você precisar manter a versão geral da API e, ao mesmo tempo, fornecer suporte para várias versões de recursos, considere uma combinação dos tipos de caminho do URI e parâmetros de consulta ou, ainda, uma abordagem de cabeçalho mais avançada.

# Combinação de caminho da URIe parâmetros de consulta
http://api.exemplo.com/v1/produtos?versao=1
http://api.exemplo.com/v1/produtos?versao=2

# Cabeçalhos estendidos, para http://api.exemplo.com/produtos
Accept: versao-da-api=1; versao-do-recurso=1
Accept: versao-da-api=1; versao-do-recurso=2

Conclusão

Tratamos de muitas coisas aqui. É hora de recapitular:

  • O versionamento da API é a prática de gerenciar de modo transparente as alterações em sua API.
  • O gerenciamento de uma API se resume a definir e evoluir contratos de dados e lidar com alterações problemáticas.
  • A maneira mais eficaz de evoluir sua API sem interromper as alterações é seguir princípios eficazes de gerenciamento de alterações de API.
  • Para a maioria das APIs, o versionamento do caminho do URI é a solução mais simples.
  • Para APIs mais complexas ou voláteis, você pode gerenciar escopos variados de alterações empregando uma combinação das abordagens de caminho do URI e de parâmetros de consulta.

Embora esses princípios devam fornecer uma direção clara sobre como gerenciar efetivamente as alterações em suas APIs, evoluir uma API é, potencialmente, mais uma arte do que uma ciência. São necessárias reflexão e previsão para criar e manter uma API confiável.