Cómo llegamos a OpenAPI
Esta es la historia sobre cómo transformamos el proceso de desarrollo de productos desde el enfoque tradicional Code First (código Primero) en la creación de la API, generando la API a partir del código, al método Design First (Diseño Primero), por lo que el código se crea a partir de un documento, basado en OpenAPI 3.0 Schema.
TL;DR
Durante mucho tiempo, estuvimos trabajando con una API WebSocket no estructurada de nuestro diseño, pero nos dimos cuenta de que era imposible seguir así y cambiamos a la especificación OpenAPI 3.0 (anteriormente Swagger). Durante la migración, quedó claro que una parte crucial de este proceso era mover el esquema de la API a un repositorio separado. La esencia de los cambios no era obtener una descripción legible de la API para una máquina, sino introducir un formato codificado y validado por software de la especificación técnica en la etapa de diseño del producto.
Experimento con un solo árbol de datos
Hace 6 años, nuestro software de transmisión de video vivía con un panel de administración en AngularJS, de donde descargabamos caóticamente la configuración y los datos de tiempo de ejecución del servidor, de alguna manera pegándolos y mostrándolos. Dado que nuestro código AngularJS se convirtió rápidamente en un desastre, comenzamos a planificar la migración a React. Decidimos probar el concepto de una estructura de árbol de renderizado en React.
Todavía no había un uso generalizado de Redux, e incluso hoy en día preferimos evitarlo, simplemente alejandonos de el.
La idea es bastante simple: React se basa en un estado de datos fijo. En React, es posible distinguir componentes que encapsulan diferentes comportamientos, como recibir los datos y mostrarlos y mover los datos hacia arriba en el árbol de componentes. Desarrollando la idea, podemos hacer de una raíz el único punto de actualización de datos. Este contiene un componente que recibe los datos del servidor y los muestra. Los datos vienen como un solo árbol y luego se representan como un todo.
Los resultados del primer experimento mostraron que actualizar los datos de las transmisiones en la IU de administración una vez por segundo (para mostrar los valores actuales de tasa de bits, número de clientes, etc.) requiere un tráfico de hasta 50 Mbps, lo cual es una locura. Sin embargo, decidimos no abandonar esta idea y resolver los problemas.
Así nació el protocolo delta, que usamos durante mucho tiempo.
El punto principal del enfoque es el siguiente: para devolver la información sobre la modificación de un objeto, basta con enviar un objeto parcial solo con campos modificados. Si es un objeto anidado, se envían objetos mini-delta con jerarquía parcial. Suena fácil, especialmente cuando pasamos de matrices a objetos en la transferencia de datos.
Por ejemplo, cambiamos la colección de flujos de una matriz a un objeto donde los nombres de los flujos son las claves. Es pan comido cuando los datos tienen claves primarias naturales, pero desafiante cuando no las hay. Además, decidimos actualizar los datos tipo matriz por completo sin actualizaciones parciales.
El servidor envía los cambios delta, o un cambio realizado en los datos iniciales, al panel de administración a través de una conexión WebSocket, lo que le brinda al cliente una imagen clara.
El mismo protocolo delta se utiliza para transmitir los datos actualizados desde la IU de administración al servidor,por ejemplo. solo se envían los datos modificados. Un árbol de datos te permite realizar una sola solicitud para una gran cantidad de cambios.
Echemos un vistazo a otro caso. Si bien un usuario puede estar editando la configuración en la IU de administración, es posible que otro ya haya guardado los mismos cambios. Entonces, cuando el primero está a punto de enviar los cambios, el botón Guardar puede desaparecer y los cambios propuestos se perderán. Este escenario se implementa fácilmente con la ayuda de dos funciones del protocolo delta: aplicar cambios y calcular cambios.
Sin embargo, cuando hay 1.000 o más transmisiones en el servidor, la cantidad de datos descargados en el panel de administración se vuelve excesiva y, en consecuencia, consume muchos recursos de la CPU. Era un problema de larga data de la UI de administración de Flussonic del que los clientes se quejaban y era complicado de resolver con ese enfoque.
Pero lo que es más importante, perdimos la oportunidad de diseñar una API pública uniforme y estandarizada para nuestros clientes. Lo discutiremos con más detalle a continuación.
Cómo diseñar una API HTTP en 2021
Anteriormente, no creamos una API sino un protocolo, manteniendo el estado interno de nuestro sistema con ambos lados: el servidor y la IU de administración. Ahora nos estamos moviendo hacia una API.
Al elegir la dirección de un mayor desarrollo, planteamos las siguientes preguntas:
- ¿Existen estándares generalmente aceptados sobre cómo realizar solicitudes HTTP para leer y modificar los datos?
- ¿Existen especificaciones generalmente aceptadas para describir un estándar como WSDL para SOAP o gRPC?
Si hoy solo hay una respuesta a la segunda pregunta: OpenAPI 3, la primera pregunta quedó incierta.
Conjunto de convenciones para la API HTTP (REST)
En primer lugar, veamos qué es REST API. Por lo general, significa API HTTP mínima pragmática, pero es una exageración. El término REST se usa para distinguirlo de la API SOAP (la comunicación entre un servidor web y un cliente se realiza a través de archivos XML con comandos y respuestas encriptados en una URL HTTP).
Además, REST no es igual a JSON API, que difiere de SOAP solo en que reemplaza XML con JSON.
La mayoría de las API REST que se usan en el campo hoy en día no son “verdaderamente REST” desde un punto de vista purista. Aunque aquí hay algunas mejores prácticas para un desarrollo de API HTTP que descubrimos:
- Usar diferentes URL HTTP para apuntar a diferentes objetos. La URL HTTP es mucho mejor que el punto final SOAP.
- Utilizar diferentes métodos HTTP para distinguir las solicitudes de lectura y modificación de datos.
- Usar códigos de respuesta HTTP para obtener lo mejor del protocolo HTTP y sus funcionalidades para la propia API.
- Utilizar JSON como formato para enviar y recibir los datos. Anteriormente, era XML, que, junto con XSLT en el navegador, permitía transformar el XML en HTML. Tenía poco sentido práctico, por lo que se quedó atrás.
Después de una década de desarrollo de HTTP y JSON, todavía no había acuerdo sobre la estandarización de solicitar y editar un objeto almacenado en otro servidor. REST en su forma original fue un intento de dar vida a esta idea, pero no prosperó.
Así que optamos por algo similar a las prácticas de Rails:
- Usar nombres simples (sustantivos en plural) para acceder a las colecciones: GET/sessions
- Usar el anidamiento lógico en un punto final para acceder a un objeto de una colección: GET(PUT, DELETE) /sessions/1 (donde “1” es una ID de sesión)
- Usar una cadena de consulta para filtrar y clasificar colecciones, pero con algunas restricciones. Si se necesita más funcionalidad, planeamos hacer una búsqueda a través de una solicitud POST y un lenguaje de consulta JSON al estilo del lenguaje de consulta MongoDB (aunque por algunas razones probablemente no lo haremos).
Una descripción más detallada de las opciones que consideramos para el filtrado y clasificación HTTP se discutirá en un artículo separado.
API primero o no?
Spoiler: sí.
Esquema del código
Ahora profundicemos en los detalles de este enfoque. Durante mucho tiempo, no entendíamos del todo el sentido de guardar y mantener los esquemas y contratos en un repositorio separado (un repositorio más) porque esto es tan molesto que tienes que editarlo aquí y allá todo el tiempo.
Siempre que una o dos personas sean responsables de que el proyecto y su API prosperen, escribir las especificaciones de la API antes que el código y almacenarlo en un repositorio separado parece ser un asunto innecesario y lento.
Puede parecer solo una cuestión de preferencia del equipo de desarrolladores (como elegir un lenguaje de programación o establecer una cantidad de espacios en la sangría) elegir generar el esquema a partir del código o viceversa: el código a partir del esquema. Sin embargo, es un error crítico.
Escribir código y luego generar un esquema a partir de este no tiene sentido. ¿Por qué no tiene sentido? Un desarrollador estructurará un programa en base a la idea que se le ocurra. Así que el esquema simplemente reflejará eso. Como resultado, no hay garantías de que este esquema no sufra un giro de 180 grados.
Code First también conduce al diseño de la API por parte del propio programador. Es decir, la tarea de diseñar la API se les asigna oralmente y se pospone hasta que la codificación esté lista. El código también puede estar escrito por diferentes personas, lo que da como resultado la mala calidad de la API. El diseño de la API se deja de lado hasta el último minuto (durante otros seis meses después de todos los plazos incumplidos).
¿Qué obtenemos al final del día? El conocimiento del producto se expresa en el código más que en la descripción formal. Documentar tal API es doloroso. Cualquier escritor técnico, tratando de encontrar algunos fragmentos de la solicitud y buscando una lista de los posibles valores de los campos, te dirá esto. Los clientes que regularmente reciben algo que no esperaban de la API también tendrán algo que decir al respecto.
Desde el punto de vista del SDLC (Software Development Life Cycle), al generar un esquema a partir del código, la etapa de diseño se acelera y la persona que diseña la API no tiene forma de crear algo válido.
Al recrear un esquema a partir del código, inevitablemente surge la tarea de examinar todo el código y recuperar todos los campos y estados de error posibles para los datos devueltos. Según nuestra experiencia, puede llevar varios meses con alrededor de quinientos millones de líneas de código.
API primero
Todo cambia cuando se edita primero el esquema de la API.
En primer lugar, tenemos la oportunidad de centrarnos en el diseño de una API agradable y conveniente para el producto sin la carga de miles de líneas de código trabajando con cada campo. En esta etapa, debes pensar en los próximos meses y años, y hay tiempo y oportunidad para eso.
La oportunidad de trabajar con calma, sin profundizar en los detalles de la implementación de la API, a un nivel que no permite cavar allí (aún no podemos escribir ningún código) hace posible realizar todo el trabajo concienzudamente y con concentración.
En segundo lugar, un repositorio de esquemas separado es crucial. Es mucho más fácil notar cualquier cambio y modificación allí, rastrear y establecer procesos que requieren revisión de colegas. El contrato sobre cómo funciona el producto ahora se almacena explícitamente, y el código lo sigue en su lugar. Los cambios de contrato son más estructurados, comprensibles y predecibles.
El desarrollador front-end ya no se volverá loco por los cambios del backend sin previo aviso. Los desarrolladores front-end pueden participar ellos mismos en este proceso de modificación.
Un gerente de producto puede escribir los requisitos del sistema de una manera que sea adecuada para programadores y programas como Postman. Un desarrollador front-end puede comenzar a trabajar en el front-end tan pronto como se modifique el esquema, sin esperar al back-end. Entonces, como escritores técnicos. También pueden escribir ejemplos y documentación allí, sin esperar al backend.
Además, puedes crear un SDK y ofrecerlo a los clientes. Puedes vivir casi tan cómodamente como hace 20 años con Corba, solo que sin el infierno binario que había allí. O casi tan cómodo como con gRPC, pero manteniendo el HTTP estándar.
OpenAPI primero
¿Cuál es el punto principal (número uno) de la transición de la generación del esquema (o la vida sin él, estos son estados cercanos) a la generación de código de acuerdo con el esquema? Ahora contamos con un conjunto de herramientas para el diseño de contratos API. El desarrollador ahora tiene criterios de aceptación claros y precisos para su asignación. Si está claro cómo diseñar un botón en la pantalla con estos datos, el trabajo está hecho. Si no, no está hecho. Los colegas pueden unirse en esta etapa y discutir algunas solicitudes de combinación exactas en Gitlab con preguntas específicas, ya comenzando a probar y experimentar con algo en la etapa de formulación de tareas. Gracias al hecho de que el esquema de API contratado (a diferencia de la formulación de prueba del problema) es en sí mismo una herramienta de software, todo eso ahora es posible.
Diseño
El proyecto contratado se convierte en un acuerdo en torno al cual se construye todo el código. Es más fácil para los programadores ir hacia el objetivo sin ahogarse en un diseño excesivo.
Es crucial distinguir una formulación de texto simple del proyecto y una contraída. ¿Qué es una novela? Es un texto que está sujeto a interpretación. Una novela, por regla general, puede ser intrincada al principio y arrugada al final, como Juego de Tronos. Este texto está escrito por personas que no son particularmente responsables de la implementación para otras personas, saben de antemano que tendrán que pensar en ello en el proceso y especialmente al final.
El enfoque API First te permite establecer los límites y los criterios de aceptación para el trabajo. El esquema está listo o no. Es mucho más fácil verificar la validez e integridad del esquema que revisar el código y restaurar todos los campos de respuesta posibles y sus valores a partir de él.
También seguimos el principio: “meses de codificación ahorran horas de desarrollo de diseño”, pero decidimos arriesgarnos e intentar lo contrario.
Trabajos paralelos
Probablemente ya hayas adivinado que un escritor técnico puede comenzar a documentar el esquema mientras aún está en construcción, es decir antes de que se escriba la primera línea de código, y comience a dar su opinión sobre lo increíble que es o viceversa.
¿Cuándo puede empezar a trabajar un probador? Así es, desde las primeras líneas del esquema, porque ahora tienen un contrato (aunque sea parcial). Es mucho más fácil interactuar con los colegas, porque: “Presioné algo y todo se rompió”. ahora se ha mejorado a “Estoy enviando una solicitud válida y recibo algo que no se indica en la especificación”.
Herramientas
Swagger parser
La primera herramienta que necesitas cuando trabajas con un esquema es @apidevtools/swagger-parser. Todas las demás herramientas se quedan cortas, por decirlo suavemente. Sin esta herramienta, ni siquiera analizarás el esquema menos complejo.
Validators
Un linter en el repositorio con esquemas es imprescindible. Si no hay linter, no se puede fusionar nada. Todas las restricciones de proyectos secundarios (su código Erlang, Rust, Go, Swift) deben reflejarse en las reglas de un linter.
openapi-ibm-validator es bueno y útil. Speccy y Spectral parecen ser más pobres sin swagger-parser.
Postman
Trata de acostumbrarte a Postman, es bueno, aunque hay dudas con respecto a su soporte nativo para OpenAPI 3.
Ahora hemos escrito nuestro propio generador de código para Erlang y lo más probable es que lo llevemos al estado de código abierto.
Mock server
Los desarrolladores frontend apreciarán el uso de servidores simulados basados en especificaciones, mientras que el backend llega con sus marcos de desarrollo rápido.
Hay un Prism, pero no nos gustó porque es demasiado privado y no nos conviene. Por lo tanto, tuvimos que hacer el nuestro propio basado en openapi-sampler y simple express.
OpenAPI OperationId
Mientras implementabamos OpenAPI, surgieron una serie de preguntas de los colegas, especialmente al recordar los hábitos establecidos. OpenAPI tiene un concepto como operationId y de hecho es algo bastante fabuloso, pero la pregunta es: “¿Por qué es necesario si hay una generación de URL manual estándar cerca?”.
OpenAPI describe una transmisión de datos sobre el protocolo HTTP, por lo que es bastante lógico utilizar las URL habituales, ya que nos esforzamos mucho en seleccionar sus nombres y los nombres de los parámetros de forma precisa y elegante, pero estos identificadores de operación coexisten con las URL.
Puede surgir una pregunta: ¿Por qué necesitamos estos dos al mismo tiempo y por qué operationId cuando hay direcciones URL claras y convenientes?
apiStreamer.init(`/streams/${name}`, 'put', null, dataForUpdate)
apiStreamer.init(`/streams/${mediaName}`, 'get')
y
client.chassis_interface_save({name}, dataForUpdate)
¿Cuál es la diferencia entre los dos? Uno usa direcciones URL en el código, el otro: ID de operación de OpenAPI.
OpenAPI proporciona más que una lista de URL y validación de parámetros. Permite describir una RPC (llamada a procedimiento remoto) completa basada en un HTTP bastante comprensible, legible y familiar.
¿Por qué el enfoque de operationId es duradero y fiable?
- Transformar operationId en funciones es más fácil de mantener y es legible por humanos. En el ejemplo de pegar las URL a mano, no solo hay un error (el nombre puede contener
/
), sino que se debe hacer un esfuerzo adicional para rastrear la correspondencia de la URL con el método con sus propios ojos.
grep
se puede usar para OperationId. Puede encontrar todas las llamadas a la API, pero no podrá hacer lo mismo con las URL hechas a mano: /streams/$$urlescape(name)>/inputs/selected_input.index - cómo escribir tal cosa para una expresión regular?
- OperationId le ofrece la oportunidad de recuperar y leer una lista completa de funciones, que también puede ser útil.
Pero todo esto eran pequeñas cosas. Lo esencial es que, en cualquier caso, todas las llamadas a la API acaban envueltas en funciones intermedias, que saben qué URL utilizar y cómo configurarlo. Teniendo en cuenta que OpenAPI describe los parámetros de entrada y salida con mucha precisión, tiene sentido no perder el tiempo escribiendo estas cosas, sino entregar este trabajo al bot para generar dicho código y ajustar su propio código internamente al modelo de datos y al modelo de control. que interactúa con este HTTP RPC.
¿Que sigue?
Tenemos alrededor de 25.000 líneas de código de esquema, y no tiene ejemplos ni descripciones. Crearemos un entorno de documentación a partir de este para compartir objetos y sus descripciones entre los proyectos. También habrá ejemplos que se utilizarán para probar los servidores simulados. ¡Mantente al tanto!
Go to the Flussonic HTTP API