GoSuda

El ecosistema de Go y OpenAPI

By iwanhae views ...

Introducción

Al desarrollar un servidor backend de producción con el lenguaje Go, casi todos los desarrolladores se encuentran, desde el primer momento, con uno de los desafíos más comunes, que es el siguiente.

¿Cómo documento la API...?

Al investigar un poco sobre este tema, se comprende que resulta beneficioso redactar la documentación conforme a la especificación OpenAPI, y de forma natural se busca una biblioteca que se integre con OpenAPI. Sin embargo, incluso después de tomar esta decisión, surge el siguiente problema.

Hay muchas bibliotecas relacionadas con OpenAPI... ¿cuál debería usar...?

Este documento es una breve introducción a las bibliotecas, escrita para los principiantes en Go que se encuentran en esta situación. Está redactado con base en el estado de finales de 2024, y dado que el ecosistema del lenguaje cambia constantemente, se recomienda consultarlo como referencia y revisar siempre las novedades más recientes.

Las estrategias de las bibliotecas frente a OpenAPI

Como probablemente ya sabe, OpenAPI es una especificación destinada a definir y documentar de forma clara las API REST. Permite definir los endpoints, las solicitudes y los formatos de respuesta de la API en formato YAML o JSON, lo cual contribuye en gran medida a automatizar la generación de código tanto para los desarrolladores de frontend como de backend, reduciendo las repeticiones innecesarias y los pequeños errores humanos.

Para integrar de forma natural esta especificación OpenAPI en un proyecto, las bibliotecas del ecosistema de Go adoptan, en líneas generales, las tres estrategias siguientes.

1. Componer la especificación de OpenAPI a partir de comentarios de Go

Una de las dificultades al desarrollar una API conforme a OpenAPI es que el documento real y el código que implementa dicho documento existen como archivos separados, en ubicaciones totalmente distintas, por lo que no es raro que se actualice el código sin actualizar el documento, o que se actualice el documento sin poder actualizar el código.

Un ejemplo sencillo sería el siguiente:

  1. Se modifica la lógica de la API en el archivo ./internal/server/user.go.
  2. La documentación real se encuentra en ./openapi3.yaml, y es posible olvidar por error realizar el cambio correspondiente.
  3. Si no se detecta este problema y se envía un Pull Request que se somete a revisión por parte de los compañeros,
  4. como los revisores tampoco pueden ver los cambios en ./openapi3.yaml, puede producirse una situación indeseada en la que la especificación de la API permanece igual mientras que la implementación real de la API cambia.

Si la documentación de la API se escribe en forma de comentarios de Go, es posible mitigar en cierta medida este problema. Como el código y la documentación se encuentran en un mismo lugar, es posible actualizar los comentarios al mismo tiempo que se modifica el código. Existen herramientas que generan automáticamente la especificación OpenAPI a partir de estos comentarios.

Un proyecto representativo es Swag. Swag analiza los comentarios del código Go y genera documentación en formato OpenAPI 2. Su uso es sencillo: basta con escribir comentarios conforme al formato definido por la biblioteca encima de la función manejadora.

 1// @Summary 유저 생성
 2// @Description 새로운 유저를 생성합니다.
 3// @Tags Users
 4// @Accept json
 5// @Produce json
 6// @Param user body models.User true "유저 정보"
 7// @Success 200 {object} models.User
 8// @Failure 400 {object} models.ErrorResponse
 9// @Router /users [post]
10func CreateUser(c *gin.Context) {
11    // ...
12}

Al escribir los comentarios de esta manera, la herramienta CLI llamada Swag analiza dichos comentarios y genera el documento OpenAPI 2. Generalmente, este proceso se ejecuta en la fase de CI, y el documento de especificación OpenAPI generado se publica en el repositorio Git, en el resultado final de la compilación o en un sistema externo de gestión de documentación de API, para su uso en la colaboración con otros proyectos.

Ventajas:

  • Como los comentarios están junto al código, es menos probable que la forma real del código y la de la documentación diverjan.
  • Sin necesidad de herramientas adicionales ni configuraciones complejas, es posible documentar de forma sencilla y flexible únicamente mediante comentarios.
  • Como los comentarios no afectan a la lógica real de la API, resultan adecuados para añadir funcionalidades temporales que no son convenientes de publicar en la documentación.

Desventajas:

  • A medida que aumenta el número de líneas de comentarios, puede disminuir la legibilidad de un único archivo de código.
  • Puede resultar difícil expresar todas las especificaciones de la API mediante comentarios.
  • Como la documentación no obliga al código a cumplir con lo que describe, no se puede garantizar que la documentación OpenAPI coincida con la lógica real.

2. Generar código Go a partir de la documentación de la especificación OpenAPI

También existe un método que sitúa la Single Source of Truth (SSOT) no en el código Go, sino en el documento. Es decir, se define primero la especificación OpenAPI y, a partir de su contenido, se genera el código Go. Como la especificación de la API genera el código, es posible imponer culturalmente que el diseño de la API se realice primero, y, al comenzar el desarrollo por la definición de la especificación de la API, se puede prevenir desde el principio la situación indeseada en la que, tras completar el desarrollo, se detectan los aspectos omitidos y todo el código debe modificarse junto con los cambios de la especificación.

Entre los proyectos representativos que adoptan este método se encuentran oapi-codegen y OpenAPI Generator. Su uso es sencillo.

  1. Se escribe un documento yaml o json conforme a la especificación OpenAPI.
  2. Se ejecuta la CLI.
  3. Se genera el código stub de Go correspondiente.
  4. Ahora solo es necesario implementar directamente la lógica detallada de cada API para que este stub pueda utilizarse.

A continuación se muestra un ejemplo del código que genera oapi-codegen.

1// StrictServerInterface represents all server handlers.
2type StrictServerInterface interface {
3	// ...
4	// Returns all pets
5	// (GET /pets)
6	FindPets(ctx context.Context, request FindPetsRequestObject) (FindPetsResponseObject, error)
7	// ...
8}

El código que oapi-codegen genera a partir de esta interfaz tiene la estructura de realizar tareas como el análisis de parámetros de consulta, encabezados y cuerpos, así como la validación, y de invocar el método adecuado declarado en la interfaz. El usuario solo necesita implementar la estructura que realiza la interfaz para completar el trabajo requerido en la implementación de la API.

Ventajas:

  • Como la especificación se define primero y luego avanza el desarrollo, resulta favorable realizar el trabajo en paralelo cuando varios equipos colaboran.
  • Como el código correspondiente a las tareas repetitivas y manuales se genera automáticamente, aumenta la eficiencia del trabajo y, al mismo tiempo, sigue siendo favorable para la depuración.
  • Resulta sencillo garantizar que la forma del documento y la del código siempre coincidan.

Desventajas:

  • Para quienes desconocen la especificación OpenAPI en sí, puede existir una curva de aprendizaje inicial considerable.
  • Como la forma del código que maneja la API se genera automáticamente según el proyecto, puede resultar difícil responder cuando se requiere personalización.

Comentario del autor. En octubre de 2024, el código Go generado por OpenAPI Generator impone no solo la lógica de la API, sino toda la estructura del proyecto, y su estructura rígida genera código poco adecuado para añadir las diversas funcionalidades que requiere un entorno de producción real. Para quienes adopten este método, se recomienda encarecidamente utilizar oapi-codegen. El autor utiliza oapi-codegen + echo + StrictServerInterface.

3. Generar la documentación de la especificación OpenAPI a partir del código Go

Cuando decenas o cientos de personas desarrollan sobre el mismo servidor, surge inevitablemente el problema de que la uniformidad entre los distintos endpoints de la API puede romperse. Como ejemplo intuitivo, si la especificación de más de 100 endpoints de API se declara en un único archivo OpenAPI yaml, dicho archivo se convertirá en un monstruo de más de 10 000 líneas, y al declarar nuevos endpoints de API es inevitable que se declare repetidamente el mismo modelo, se omitan algunos campos o surjan nombres de rutas que no cumplen con las convenciones, y así comience a romperse la uniformidad general de la API.

Para resolver este problema se podrían tomar medidas como asignar un responsable específico para gestionar el yaml de OpenAPI o desarrollar un Linter que detecte automáticamente estos problemas durante el proceso de CI. Sin embargo, también es posible definir un Domain-specific language (DSL) en Go para forzar que todas las API mantengan una uniformidad coherente.

Un proyecto representativo que utiliza esta técnica es Kubernetes (que la implementa de forma propia, sin bibliotecas adicionales), y también es posible utilizar proyectos como go-restful y goa. A continuación se muestra un ejemplo 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})

Al escribir código Go compilable como el anterior, se obtiene la ventaja de que la implementación de la API POST /users y la definición de su documentación se completan simultáneamente.

Ventajas:

  • Como todo se genera a partir del código, resulta sencillo mantener la consistencia de la API en todo el proyecto.
  • Al aprovechar el sistema de tipos estricto de Go, es posible obtener una especificación más precisa y menos controvertida que la que se obtiene al utilizar todas las funciones de OpenAPI3.

Desventajas:

  • Es necesario aprender el DSL definido por cada framework, y puede resultar difícil aplicarlo a código existente.
  • Como es necesario seguir obligatoriamente las reglas propuestas por el framework, la libertad y la flexibilidad pueden disminuir.

Reflexión final

Cada método tiene sus ventajas y desventajas, y es importante elegir el más adecuado según los requisitos del proyecto y las preferencias del equipo. Lo más importante no es qué método es mejor, sino realizar un juicio de valor sobre cuál es la solución más adecuada para la situación en la que uno se encuentra, y mantener una alta productividad en el desarrollo para disfrutar de una salida temprana del trabajo y de un equilibrio satisfactorio entre la vida laboral y personal.

Aunque este artículo se haya redactado con base en octubre de 2024, los ecosistemas de Go y OpenAPI evolucionan continuamente, por lo que se recomienda dar seguimiento a las novedades de cada biblioteca y proyecto, así como a sus cambios en ventajas y desventajas, teniendo en cuenta el tiempo transcurrido desde la lectura de este artículo.

¡Que tenga una feliz vida con Go! 😘