GoSuda

Go e o ecossistema OpenAPI

By iwanhae
views ...

Introdução

Ao desenvolver um servidor backend de produção em Go, um dos primeiros desafios que a maioria dos desenvolvedores enfrenta é o seguinte:

Como documentar APIs...?

Ao pesquisar um pouco sobre o assunto, percebe-se que é vantajoso escrever documentação em conformidade com a especificação OpenAPI e, naturalmente, busca-se uma biblioteca que se integre com o OpenAPI. No entanto, mesmo após essa decisão, surge o seguinte problema:

Existem muitas bibliotecas relacionadas ao OpenAPI... Qual usar...?

Este documento é uma breve introdução a bibliotecas, escrita para iniciantes em Go que estão enfrentando essa situação. Foi escrito no final de 2024 e, como o ecossistema de linguagens está sempre em fluxo, é recomendável consultar as informações mais recentes enquanto o utiliza como referência.

Estratégias das Bibliotecas para Abordar o OpenAPI

Como você já deve saber, o OpenAPI é uma especificação para definir e documentar APIs REST de forma clara. Ele define os endpoints, as requisições e os formatos de resposta das APIs em formato YAML ou JSON, o que auxilia não apenas os desenvolvedores, mas também a front-end e a back-end, automatizando a geração de código, reduzindo repetições desnecessárias e erros humanos.

Para integrar o OpenAPI de forma natural com projetos, as bibliotecas no ecossistema Go adotam principalmente três estratégias.

1. Combinação de Anotações Go em Documentação de Especificação OpenAPI

Um dos pontos difíceis ao desenvolver APIs com base no OpenAPI é que o código real e a documentação correspondente estão em arquivos separados em locais completamente diferentes. Isso leva a situações em que o código é atualizado, mas a documentação não, ou vice-versa.

Por exemplo:

  1. Você modifica a lógica de uma API em um arquivo como ./internal/server/user.go.
  2. A documentação real existe em ./openapi3.yaml, e você pode esquecer de atualizar essa documentação.
  3. Se você enviar um Pull Request sem perceber esse problema,
  4. Os revisores também podem não notar a ausência de mudanças em ./openapi3.yaml. Isso pode resultar em uma situação em que a especificação da API permanece a mesma, mas a implementação real da API é modificada.

Escrever a documentação da API na forma de anotações Go pode mitigar esse problema. Como o código e a documentação estão em um só lugar, você pode atualizar as anotações ao modificar o código. Existem ferramentas que geram automaticamente a documentação de especificação OpenAPI com base nessas anotações.

Um projeto representativo é o Swag. O Swag analisa as anotações no código Go e gera documentos no formato OpenAPI 2. O uso é simples: basta adicionar anotações no formato especificado por cada biblioteca acima das funções de manipulação.

 1// @Summary Cria usuário
 2// @Description Cria um novo usuário.
 3// @Tags Users
 4// @Accept json
 5// @Produce json
 6// @Param user body models.User true "Informações do usuário"
 7// @Success 200 {object} models.User
 8// @Failure 400 {object} models.ErrorResponse
 9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11    // ...
12}

Ao escrever anotações dessa forma, a CLI Swag analisa essas anotações e gera documentos OpenAPI 2. Geralmente, essa ação ocorre durante o processo de CI, e a documentação de especificação OpenAPI gerada é distribuída para o repositório Git, para o resultado final da compilação ou para um sistema externo de gerenciamento de documentação de API para ser utilizada em colaborações com outros projetos.

Vantagens:

  • Como as anotações estão junto com o código, a possibilidade de divergência entre o código real e a documentação diminui.
  • A documentação pode ser feita de forma simples e livre, apenas com anotações, sem ferramentas separadas ou configurações complexas.
  • Como as anotações não afetam a lógica da API real, é uma boa opção para adicionar funcionalidades temporárias que você não quer tornar públicas na documentação.

Desvantagens:

  • O número de linhas de anotações pode reduzir a legibilidade de arquivos de código individuais.
  • Pode ser difícil expressar todas as especificações da API na forma de anotações.
  • Como a documentação não força o código, não há garantia de que a documentação OpenAPI e a lógica real coincidam.

2. Geração de Código Go a partir de Documentos de Especificação OpenAPI

Outra abordagem é colocar a fonte única de verdade (SSOT) na documentação, em vez do código Go. Essa abordagem consiste em definir primeiro a especificação OpenAPI e, em seguida, gerar o código Go com base no conteúdo definido. Como a especificação da API gera o código, você pode forçar culturalmente o design da API como a primeira etapa, e definir a especificação da API se torna o ponto de partida do desenvolvimento. Isso tem a vantagem de evitar a situação em que partes perdidas são percebidas somente após a conclusão do desenvolvimento e todo o código é modificado junto com a alteração da especificação da API.

Os projetos representativos que adotam essa abordagem são oapi-codegen e OpenAPI Generator. O uso é simples:

  1. Escreva um documento YAML ou JSON de acordo com a especificação OpenAPI.
  2. Execute a CLI.
  3. Um código stub Go correspondente será gerado.
  4. Agora, basta implementar a lógica detalhada para cada API para que este stub possa ser usado.

O exemplo a seguir mostra o código gerado pelo oapi-codegen.

1// StrictServerInterface representa todos os manipuladores do servidor.
2type StrictServerInterface interface {
3	// ...
4	// Retorna todos os pets
5	// (GET /pets)
6	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7	// ...
8}

Através dessa interface, o código gerado pelo oapi-codegen executa a lógica de análise de parâmetros de consulta, cabeçalhos, corpos e validação e chama o método apropriado declarado na interface. O usuário só precisa implementar a implementação dessa interface para concluir o trabalho necessário para a implementação da API.

Vantagens:

  • Como a especificação vem primeiro e o desenvolvimento prossegue depois, é vantajoso para realizar o trabalho em paralelo em equipes que colaboram.
  • O código para as partes que costumavam ser tarefas repetitivas é gerado automaticamente, o que aumenta a eficiência do trabalho e ainda é vantajoso para a depuração.
  • É fácil garantir que a forma da documentação e do código sempre corresponda.

Desvantagens:

  • Se você não estiver familiarizado com a especificação OpenAPI, haverá uma curva de aprendizado inicial.
  • Como a forma do código que manipula a API é gerada automaticamente pelo projeto, pode ser difícil responder caso seja necessária personalização.

Comentário do autor. Em outubro de 2024, o código Go gerado pelo OpenAPI Generator força não apenas a lógica da API, mas também a forma de todo o projeto, e a estrutura do projeto é rígida. Portanto, ele gera um código inadequado para adicionar várias funcionalidades necessárias no ambiente de produção real. Se você adotar esta abordagem, recomendo vivamente o uso do oapi-codegen. O autor está usando oapi-codegen + echo + StrictServerInterface.

3. Geração de Documentos de Especificação OpenAPI com Código Go

Um problema que inevitavelmente surge quando dezenas ou centenas de pessoas estão trabalhando no mesmo servidor é a quebra da uniformidade de cada API individual. Um exemplo intuitivo é que, se você declarar as especificações de mais de 100 endpoints de API em um único arquivo OpenAPI YAML, esse arquivo se tornará um monstro de mais de 10.000 linhas. E, ao declarar um novo endpoint de API, você inevitavelmente declarará o mesmo modelo repetidamente, perderá alguns campos ou criará um nome de caminho que não esteja em conformidade com as convenções. Isso começará a quebrar a uniformidade geral da API.

Para resolver esse problema, você pode ter um proprietário separado para gerenciar o OpenAPI YAML ou desenvolver um linter para que ele possa ser detectado automaticamente durante o processo de CI, mas você pode definir uma linguagem específica de domínio (DSL) em Go para forçar todas as APIs a ter uma consistência uniforme.

Um projeto representativo que usa essa técnica é o Kubernetes (construído internamente sem uma biblioteca separada), e você pode usá-la com projetos como go-restful e goa. Aqui está um exemplo de uso de goa:

 1var _ = Service("user", func() {
 2    Method("create", func() {
 3        Payload(UserPayload)
 4        Result(User)
 5        HTTP(func() {
 6            POST("/users")
 7            Response(StatusOK)
 8        })
 9    })
10})

Ao escrever um código Go compilável como este, você pode obter a vantagem de que a implementação da API POST /users e a definição da documentação sejam concluídas ao mesmo tempo.

Vantagens:

  • Como tudo vem do código, é fácil manter a consistência da API em todo o projeto.
  • Ao usar o sistema de tipo forte do Go, você pode obter uma especificação mais precisa e incontroversa do que ao usar todas as funcionalidades do OpenAPI3.

Desvantagens:

  • Você precisa aprender a DSL definida em cada framework, e pode ser difícil de aplicar ao código existente.
  • Como você tem que seguir as regras propostas pelo framework à força, a liberdade e a flexibilidade podem ser reduzidas.

Concluindo

Cada método tem vantagens e desvantagens, e é importante escolher o método apropriado, dependendo dos requisitos do projeto e das preferências da equipe. O mais importante não é qual método é melhor, mas sim avaliar qual é a solução mais adequada para a sua situação atual e aumentar a produtividade do desenvolvimento, para que você possa sair do trabalho mais cedo e desfrutar de um equilíbrio satisfatório entre trabalho e vida pessoal.

Este artigo foi escrito em outubro de 2024, mas o ecossistema Go e OpenAPI está em constante evolução, por isso, considere o intervalo de tempo entre a escrita deste artigo e a sua leitura, e acompanhe continuamente as novidades das bibliotecas e projetos e as suas vantagens e desvantagens.

Tenha uma vida Go feliz! 😘