Capítulo 1. Diseñar, construir y especificar API
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Se te presentarán muchas opciones cuando diseñes y construyas API. Es increíblemente rápido construir un servicio con tecnologías y marcos modernos, pero crear un enfoque duradero requiere una reflexión y una consideración cuidadosas. En este capítulo exploraremos REST y RPC para modelar las relaciones productor y consumidor en el caso práctico.
Descubrirás cómo las normas pueden ayudar a acortar las decisiones de diseño y alejarse de posibles problemas de compatibilidad. Verás las especificaciones OpenAPI, los usos prácticos para los equipos y la importancia del versionado.
Las interacciones basadas en RPC se especifican utilizando un esquema; para comparar y contrastar con un enfoque REST, exploraremos gRPC. Teniendo en cuenta tanto REST como gRPC, estudiaremos los distintos factores que hay que tener en cuenta a la hora de modelar los intercambios. Estudiaremos la posibilidad de proporcionar tanto una API REST como RPC en el mismo servicio y si es lo correcto.
Caso práctico: Diseño de la API de asistentes
En la Introducción decidimos migrar nuestro sistema de conferencias heredado y pasar a una arquitectura más basada en API. Como primer paso para realizar este cambio, vamos a crear un nuevo servicio de Asistentes, que expondrá una API de Asistentes correspondiente. También ofrecimos una definición restringida de una API. Para diseñar con eficacia, tenemos que considerar más ampliamente el intercambio entre el productor y el consumidor, y lo que es más importante, quiénes son el productor y el consumidor. El productor es el equipo de asistentes. Este equipo mantiene dos relaciones clave:
-
El equipo de asistentes es el propietario del productor, y el equipo de la conferencia heredada es el propietario del consumidor. Existe una estrecha relación entre estos dos equipos y cualquier cambio en la estructura se coordina fácilmente. Es posible lograr una fuerte cohesión entre los servicios de productor/consumidor.
-
El equipo de asistentes es el propietario del productor, y el equipo del sistema externo de PPC es el propietario del consumidor. Existe una relación entre los equipos, pero cualquier cambio debe coordinarse para no romper la integración. Se requiere un acoplamiento laxo y habría que gestionar cuidadosamente los cambios de ruptura.
Compararemos y contrastaremos los enfoques para diseñar y construir la API de asistentes a lo largo de este capítulo .
Introducción a REST
REpresentation State Transfer (REST) es un conjunto de restricciones arquitectónicas, que se aplican más comúnmente utilizando HTTP como protocolo de transporte subyacente. La disertación de Roy Fielding "Architectural Styles and the Design of Network-based Software Architectures" proporciona una definición completa de REST. Desde una perspectiva práctica, para ser considerada RESTful tu API debe garantizar que:
-
Se modela una interacción productor-consumidor en la que el productor modela recursos con los que el consumidor puede interactuar.
-
Las solicitudes del productor al consumidor son sin estado, lo que significa que el productor no almacena en caché los detalles de una solicitud anterior. Para construir una cadena de peticiones sobre un recurso determinado, el consumidor debe enviar la información necesaria al productor para que la procese.
-
Las peticiones son almacenables en caché, lo que significa que el productor puede proporcionar pistas al consumidor cuando sea apropiado. En HTTP esto se suele proporcionar en la información contenida en la cabecera.
-
Se transmite al consumidor una interfaz uniforme. En breve explorarás el uso de verbos, recursos y otros patrones.
-
Es un sistema por capas, que abstrae la complejidad de los sistemas que se encuentran detrás de la interfaz REST. Por ejemplo, el consumidor no debe saber ni preocuparse de si está interactuando con una base de datos o con otros servicios.
Introducción a REST y HTTP con ejemplos
Veamos un ejemplo de REST sobre HTTP. El siguiente intercambio es una solicitud GET, donde GET representa el método o verbo. Un verbo como GET describe la acción a realizar sobre un recurso concreto; en este ejemplo, consideramos el recurso attendees. Se pasa una cabecera Accept para definir el tipo de contenido que el consumidor desea recuperar. REST define la noción de representación en el cuerpo y permite definir metadatos de representación en las cabeceras.
En los ejemplos de este capítulo, representamos una petición encima del separador ---
y una respuesta debajo:
GET http://mastering-api.com/attendees Accept: application/json --- 200 OK Content-Type: application/json { "displayName": "Jim", "id": 1 }
La respuesta incluye el código de estado y el mensaje del servidor, lo que permite al consumidor interrogar el resultado de la operación en el recurso del lado del servidor. El código de estado de esta solicitud fue un 200 OK, lo que significa que la solicitud fue procesada con éxito por el productor. En el cuerpo de la respuesta se devuelve una representación JSON que contiene los asistentes a la conferencia.
Muchos tipos de contenido son válidos para ser devueltos desde un REST, sin embargo, es importante tener en cuenta si el tipo de contenido es analizable por el consumidor. Por ejemplo, devolver application/pdf
es válido, pero no representaría un intercambio que pudiera ser utilizado fácilmente por otro sistema. Más adelante en este capítulo exploraremos enfoques para modelar tipos de contenido, centrándonos principalmente en JSON.
Nota
REST es relativamente sencillo de implementar porque la relación entre el cliente y el servidor es sin estado, lo que significa que el servidor no conserva ningún estado del cliente. El cliente debe devolver el contexto al servidor en las solicitudes posteriores; por ejemplo, una solicitud dehttp://mastering-api.com/attendees/1 recuperaría más información sobre un asistente concreto de.
El modelo de madurez de Richardson
Hablando en en la QCon de 2008, Leonard Richardson presentó sus experiencias de revisión de muchas API REST. Richardson encontró niveles de adopción que los equipos aplican a la creación de API desde una perspectiva REST. Martin Fowler también trató la heurística de madurez de Richardson en su blog. La Tabla 1-1 explora los distintos niveles representados por la heurística de madurez de Richardson y su aplicación a las API RESTful.
Nivel 0 - HTTP/RPC |
Establece que la API se construye utilizando HTTP y tiene la noción de una única URI. Tomando nuestro ejemplo anterior de |
Nivel 1 - Recursos |
Establece el uso de recursos y empieza a introducir la idea de modelar los recursos en el contexto de la URI. En nuestro ejemplo, si añadiéramos |
Nivel 2 - Verbos (Métodos) |
Empieza a introducir el modelado correcto de múltiples URI de recursos a los que se accede por diferentes métodos de solicitud (también conocidos como verbos HTTP), basándose en el efecto de los recursos sobre el servidor. Una API de nivel 2 puede ofrecer garantías sobre los métodos |
Nivel 3 - Controles hipermedia |
Éste es el epítome del diseño REST e implica API navegables mediante el uso de HATEOAS (Hypertext As The Engine Of Application State). En nuestro ejemplo, cuando llamamos a |
Al diseñar intercambios de API, es importante tener en cuenta los distintos niveles de Madurez de Richardson. Avanzar hacia el nivel 2 te permitirá proyectar un modelo de recursos comprensible para el consumidor, con acciones adecuadas disponibles contra el modelo. A su vez, esto reduce el acoplamiento y oculta todos los detalles del servicio de respaldo. Más adelante veremos también cómo se aplica esta abstracción al versionado.
Si el consumidor es el equipo de la PPC, modelar un intercambio con bajo acoplamiento y proyectar un modelo RESTful sería un buen punto de partida. Si el consumidor es el equipo de la conferencia heredada, aún podemos optar por utilizar una API RESTful, pero también existe otra opción con RPC. Para empezar a considerar este tipo de modelado tradicionalmente este-oeste, exploraremos RPC.
Introducción a las API de Llamada a Procedimiento Remoto (RPC)
Una Llamada a Procedimiento Remoto (RPC) implica llamar a un método en un proceso pero hacer que ejecute código en otro proceso. Mientras que REST puede proyectar un modelo del dominio y proporciona una abstracción de la tecnología subyacente al consumidor, RPC implica exponer un método de un proceso y permitir que se llame directamente desde otro.
gRPC es un RPC moderno de código abierto y alto rendimiento. gRPC está bajo la administración de la Fundación Linux y es el estándar de facto para RPC en la mayoría de las plataformas.La Figura 1-1 describe una llamada RPC en gRPC, que implica que el servicio de conferencia heredado invoque el método remoto en el servicio Asistente. El servicio gRPC Attendee inicia y expone un servidor gRPC en un puerto especificado, lo que permite invocar métodos de forma remota. En el lado del cliente (el servicio de conferencia heredado), se utiliza un stub para abstraer la complejidad de realizar la llamada remota en la biblioteca. gRPC requiere un esquema para cubrir completamente la interacción entre el productor y el consumidor.
Una diferencia clave entre REST y RPC es el estado. REST es, por definición, sin estado; con RPC, el estado depende de la implementación. Las integraciones basadas en RPC, en determinadas situaciones, también pueden acumular estado como parte del intercambio. Esta acumulación de estado tiene la conveniencia de un alto rendimiento, con el coste potencial de la fiabilidad y las complejidades de enrutamiento. Con RPC, el modelo tiende a transmitir la funcionalidad exacta a nivel de método que se requiere de un servicio secundario. Esta opcionalidad en el estado puede conducir a un intercambio potencialmente más acoplado entre productor y consumidor. El acoplamiento no siempre es malo, especialmente en los servicios este-oeste, donde el rendimiento es una consideración clave.
Breve mención a GraphQL
Antes de explorar en detalle los estilos REST y RPC en, no podemos dejar de mencionar GraphQL y su lugar en el mundo de las API. RPC ofrece acceso a una serie de funciones individuales proporcionadas por un productor, pero no suele extender un modelo o abstracción al consumidor. REST, en cambio, extiende un modelo de recursos para una única API proporcionada por el productor. Es posible ofrecer varias API en la misma URL base utilizando pasarelas API. Exploraremos más a fondo esta noción en el Capítulo 3. Si ofrecemos varias API de esta forma, el consumidor tendrá que consultar secuencialmente para construir el estado en el lado del cliente. El consumidor también necesita comprender la estructura de todos los servicios implicados en la consulta. Este enfoque es un desperdicio si el consumidor sólo está interesado en un subconjunto de campos de la respuesta. Los dispositivos móviles están limitados por pantallas más pequeñas y por la disponibilidad de la red, por lo que GraphQL es excelente en este escenario.
GraphQL introduce una capa tecnológica sobre los servicios, almacenes de datos y API existentes, que proporciona un lenguaje de consulta para consultar a través de múltiples fuentes. El lenguaje de consulta permite al cliente pedir exactamente los campos requeridos, incluidos los campos que abarcan varias API. GraphQL utiliza el lenguaje de esquema GraphQL, para especificar los tipos en las API individuales y cómo se combinan las API. Una gran ventaja de introducir un esquema GraphQL en tu sistema es la capacidad de proporcionar una única versión en todas las API, eliminando la necesidad de una gestión de versiones potencialmente compleja en el lado del consumidor.
GraphQL destaca cuando un consumidor necesita un acceso uniforme a la API a través de una amplia gama de servicios interconectados. El esquema proporciona la conexión y amplía el modelo de dominio, permitiendo al cliente especificar exactamente lo que necesita en el lado del consumidor. Esto funciona muy bien para modelar una interfaz de usuario y también sistemas de informes o sistemas de almacenamiento de datos. En los sistemas en los que se almacenan grandes cantidades de datos en diferentes subsistemas, GraphQL puede proporcionar una solución ideal para abstraer la complejidad interna del sistema.
Es posible colocar GraphQL sobre los sistemas heredados existentes y utilizarlo como fachada para ocultar la complejidad, aunque proporcionar GraphQL sobre una capa de API bien diseñadas a menudo significa que la fachada es más sencilla de implementar y mantener. GraphQL puede considerarse una tecnología complementaria y debe tenerse en cuenta a la hora de diseñar y construir API. GraphQL también puede considerarse un enfoque completo para construir todo un ecosistema de API.
GraphQL brilla en determinados escenarios y te animamos a que eches un vistazo a Learning GraphQL (O'Reilly) y GraphQL in Action (O'Reilly) para profundizar en este tema.
Normas y estructura de la API REST
REST tiene algunas reglas muy básicas, pero en su mayor parte la implementación y el diseño se dejan como ejercicio para el desarrollador. Por ejemplo, ¿cuál es la mejor forma de transmitir errores? ¿Cómo debe implementarse la paginación? ¿Cómo se evita accidentalmente construir una API en la que la compatibilidad se rompa con frecuencia? Llegados a este punto, es útil tener una definición más práctica en torno a las API para proporcionar uniformidad y expectativas a través de diferentes implementaciones. Aquí es donde las normas o directrices pueden ayudar, sin embargo hay una gran variedad de fuentes entre las que elegir.
Para hablar del diseño, utilizaremos las Directrices de la API REST de Microsoft, que representan una serie de directrices internas que se han hecho públicas. Las directrices utilizan la RFC-2119, que define la terminología para normas como MUST, SHOULD, SHOULD NOT, MUST NOT, etc., permitiendo al desarrollador determinar si los requisitos son opcionales u obligatorios.
Consejo
Como las normas de las API REST están evolucionando, en la página de Github del libro hay una lista abierta de normas de API. Por favor, aporta mediante pull request cualquier norma abierta que creas que sería útil que otros lectores tuvieran en cuenta.
Consideremos el diseño de la API de asistentes utilizando las directrices de la API REST de Microsoft e introduzcamos un punto final para crear un nuevo attendee
. Si estás familiarizado con REST, pensarás inmediatamente en utilizar POST
:
POST http://mastering-api.com/attendees { "displayName": "Jim", "givenName": "James", "surname": "Gough", "email": "jim@mastering-api.com" } --- 201 CREATED Location: http://mastering-api.com/attendees/1
El encabezado Ubicación revela la ubicación del nuevo recurso creado en el servidor, y en esta API estamos modelando un ID único para el usuario. Es posible utilizar el campo de correo electrónico como ID único, sin embargo, las Directrices de la API REST de Microsoft recomiendan en la sección 7.9 que la información de identificación personal (IIP) no forme parte de la URL.
Advertencia
La razón para eliminar los datos sensibles de la URL es que las rutas o los parámetros de consulta podrían almacenarse inadvertidamente en la red, por ejemplo, en los registros del servidor o en cualquier otro lugar.
Otro aspecto de las API que puede resultar difícil es la nomenclatura. Como trataremos en "Versionado de API", algo tan sencillo como cambiar un nombre puede romper la compatibilidad. Hay una breve lista de nombres estándar que deberían utilizarse en las Directrices de API REST de Microsoft, sin embargo, los equipos deberían ampliarla para disponer de un diccionario de datos de dominio común que complemente las normas. En muchas organizaciones es increíblemente útil investigar proactivamente los requisitos en torno al diseño de datos y, en algunos casos, a la gobernanza. Las organizaciones que proporcionan coherencia en todas las API que ofrece una empresa presentan una uniformidad que permite a los consumidores comprender y conectar las respuestas. En algunos dominios puede que ya exista una terminología ampliamente conocida: ¡úsala en !
Colecciones y Paginación
Parece razonable modelar la solicitud GET /attendees
como una respuesta que contenga una matriz sin procesar. El siguiente fragmento de código fuente muestra un ejemplo de lo que podría parecer como cuerpo de respuesta:
GET http://mastering-api.com/attendees --- 200 OK [ { "displayName": "Jim", "givenName": "James", "surname": "Gough", "email": "jim@mastering-api.com", "id": 1, }, ... ]
Consideremos un modelo alternativo a la solicitud GET /attendees
que anida el array de asistentes dentro de un objeto. Puede parecer extraño que una respuesta de array se devuelva en un objeto, sin embargo, la razón de ello es que nos permite modelar colecciones más grandes y la paginación. La paginación consiste en devolver un resultado parcial, al tiempo que se proporcionan instrucciones sobre cómo puede solicitar el consumidor el siguiente conjunto de resultados. Esto es aprovechar las ventajas de la retrospectiva; añadir paginación más adelante y convertir de un array a un objeto para añadir un @nextLink
(como recomiendan las normas) rompería la compatibilidad:
GET http://mastering-api.com/attendees --- 200 OK { "value": [ { "displayName": "Jim", "givenName": "James", "surname": "Gough", "email": "jim@mastering-api.com", "id": 1, } ], "@nextLink": "{opaqueUrl}" }
Filtrar colecciones
Nuestra conferencia se ve un poco solitaria con un solo asistente, sin embargo, cuando las colecciones crezcan en tamaño puede que necesitemos añadir filtrado además de paginación. El estándar de filtrado proporciona un lenguaje de expresión dentro de REST para estandarizar cómo deben comportarse las consultas de filtrado, basándose en el estándar OData. Por ejemplo, podríamos encontrar a todos los asistentes con el displayName
Jim utilizando:
GET http://mastering-api.com/attendees?$filter=displayName eq 'Jim'
No es necesario completar todas las funciones de filtrado y búsqueda desde el principio. Sin embargo, diseñar una API de acuerdo con los estándares permitirá al desarrollador soportar una arquitectura de API en evolución sin romper la compatibilidad para los consumidores. El filtrado y la consulta es una función para la que GraphQL es realmente bueno, especialmente si la consulta y el filtrado a través de muchos de tus servicios se convierte en relevante.
Tratamiento de errores
Una consideración importante a la hora de ampliar las API a los consumidores es definir qué debe ocurrir en diversos escenarios de error. Es útil definir por adelantadolas normas de error y compartirlas con los productores para proporcionar coherencia. Es importante que los errores describan al consumidor exactamente qué ha ido mal en la solicitud, ya que así se evitará aumentar el soporte necesario para la API.
Las directrices establecen que "Para las condiciones de no éxito, los desarrolladores DEBERÍAN ser capaces de escribir una pieza de código que gestione los errores de forma coherente"Debe proporcionarse al consumidor un código de estado preciso, porque a menudo los consumidores construirán la lógica en torno al código de estado proporcionado en la respuesta. Hemos visto muchas API que devuelven errores en el cuerpo junto con una respuesta de tipo 2xx, que se utiliza para indicar éxito. Los códigos de estado 3xx para redirecciones son seguidos activamente por algunas implementaciones de bibliotecas consumidoras, lo que permite a los proveedores reintentar y acceder a fuentes externas.
4xx suele indicar un error en el lado del cliente; en este punto, el contenido del campo message
es extremadamente útil para el desarrollador o el usuario final. 5xx suele indicar un fallo en el lado del servidor y algunas bibliotecas cliente reintentarán en este tipo de fallos. Es importante considerar y documentar qué ocurre en el servicio en función de un fallo inesperado; por ejemplo, en un sistema de pago, ¿un 500 significa que el pago se ha realizado o no?
Advertencia
Asegúrate de que los mensajes de error enviados de vuelta a un consumidor externo no contengan trazas de pila ni otra información sensible. Esta información puede ayudar a un pirata informático que pretenda comprometer el sistema. La estructura de error de las directrices de Microsoft tiene el concepto de InnerError, que podría ser útil para colocar trazas de pila/descripciones de problemas más detalladas. Esto sería increíblemente útil para la depuración, pero debe eliminarse antes de que llegue a un consumidor externo.
Apenas hemos arañado la superficie de la construcción de API REST, pero está claro que hay que tomar muchas decisiones importantes cuando se empieza a construir una API. Si combinamos el deseo de presentar API intuitivas que sean coherentes y permitan una API evolutiva y compatible, merece la pena adoptar pronto un estándar de API.
Directriz ADR: Elegir una norma API
Para que tu decisión sobre las normas API, la directriz de la Tabla 1-2 enumera temas importantes a tener en cuenta. Hay una serie de directrices entre las que elegir, incluidas las de Microsoft que se comentan en esta sección, y encontrar la que mejor se adapte a los estilos de API que se producen es una decisión clave.
Decisión |
¿Qué norma API debemos adoptar? |
Puntos de debate |
¿Tiene ya la organización otras normas dentro de la empresa? ¿Podemos extender esas normas a los consumidores externos? ¿Utilizamos alguna API de terceros que tengamos que exponer a un consumidor (por ejemplo, Servicios de Identidad) que ya tenga una norma? ¿Cómo afecta a nuestros consumidores el hecho de no tener una norma? |
Recomendaciones |
Elige la norma API que mejor se adapte a la cultura de la organización y a los formatos de API que ya tengas en el inventario. Prepárate para evolucionar y añadir a una norma cualquier modificación específica de un ámbito o sector. Empieza con algo al principio para evitar tener que romper la compatibilidad más tarde por coherencia. Sé crítico con las API existentes. ¿Están en un formato que los consumidores entenderían o se requiere más esfuerzo para ofrecer el contenido? |
Especificar API REST utilizando OpenAPI
Como estamos empezando a ver, el diseño de una API es fundamental para el éxito de una plataforma API. La siguiente consideración que trataremos es compartir la API con los desarrolladores que consumen nuestras API.
Los mercados de API proporcionan un listado público o privado de API disponibles para un consumidor. Un desarrollador puede examinar la documentación y probar rápidamente una API en el navegador para explorar su comportamiento y funcionalidad. Los mercados de API públicos y privados han colocado a las API REST en un lugar destacado en el espacio del consumidor. El éxito de las API REST se ha visto impulsado tanto por el panorama técnico como por la baja barrera de entrada tanto para el cliente como para el servidor.
A medida que crecía el número de API, rápidamente se hizo necesario disponer de un mecanismo para compartir la forma y la estructura de las API con los consumidores. Por eso, los líderes del sector de las API formaron la Iniciativa OpenAPI para construir la Especificación OpenAPI (OAS). Swagger fue la implementación de referencia original de la Especificación OpenAPI, pero ahora la mayoría de las herramientas han convergido en el uso de OpenAPI.
Las Especificaciones OpenAPI son representaciones de la API basadas en JSON o YAML que describen la estructura, los objetos de dominio intercambiados y cualquier requisito de seguridad de la API. Además de la estructura, también transmiten metadatos sobre la API, incluido cualquier requisito legal o de licencia, y también llevan documentación y ejemplos que son útiles para los desarrolladores que consumen la API. Las Especificaciones OpenAPI son un concepto importante en torno a las modernas API REST, y se han creado muchas herramientas y productos en torno a su uso .
Aplicación práctica de las especificaciones OpenAPI
Una vez que se comparte una OAS, la potencia de la especificación empieza a hacerse patente. OpenAPI.Tools documenta toda una serie de herramientas de código abierto y cerrado disponibles. En esta sección exploraremos algunas de las aplicaciones prácticas de las herramientas basadas en su interacción con la Especificación OpenAPI.
Utilizar algunas de las siguientes aplicaciones prácticas puede ayudar tanto a mejorar la experiencia del desarrollador como a garantizar la salud del intercambio.
Generación de código
Quizás una de las características más útiles de una OAS es permitir la generación de código del lado del cliente para consumir la API. Como ya hemos comentado, podemos incluir todos los detalles del servidor, la seguridad y, por supuesto, la propia estructura de la API. Con toda esta información podemos generar una serie de objetos modelo y de servicio que representen e invoquen a la API. El proyecto Generador de OpenAPI admite una amplia gama de lenguajes y cadenas de herramientas. Por ejemplo, en Java puedes optar por utilizar Spring o JAX-RS y en TypeScript puedes elegir una combinación de TypeScript con tu framework favorito. También es posible generar los stubs de implementación de la API a partir del OAS.
Esto plantea una pregunta importante: ¿qué debe ir primero, la especificación o el código del lado del servidor? En el Capítulo 2, hablamos del "seguimiento de contratos", que presenta un enfoque basado en el comportamiento para probar y crear API. El problema de las especificaciones OpenAPI es que, por sí solas, sólo transmiten la forma de la API. Las especificaciones OpenAPI no modelan completamente la semántica (o el comportamiento esperado) de la API en diferentes condiciones. Si vas a presentar una API a usuarios externos, es importante que se modele y pruebe la gama de comportamientos para evitar tener que cambiar drásticamente la API más adelante.
Las API deben diseñarse desde la perspectiva del consumidor y tener en cuenta la necesidad de abstraer la representación subyacente para reducir el acoplamiento. Es importante poder refactorizar libremente los componentes entre bastidores sin romper la compatibilidad de la API, de lo contrario la abstracción de la API pierde valor.
Validación OpenAPI
Especificaciones OpenAPI son útiles para validar el contenido de un intercambio y asegurarse de que la solicitud y la respuesta coinciden con las expectativas de la especificación. Al principio puede no parecer evidente dónde sería útil esto: si se genera código, seguramente el intercambio siempre será correcto. Una aplicación práctica de la validación OpenAPI es la seguridad de las API y de la infraestructura de las API. En muchas organizaciones es común una arquitectura zonal, con una noción de zona desmilitarizada (DMZ) utilizada para proteger una red del tráfico entrante. Una función útil es interrogar los mensajes en la DMZ y terminar el tráfico si la especificación no coincide. Trataremos la seguridad con más detalle en el Capítulo 6.
Atlassian, por ejemplo, ha abierto una herramienta llamada swagger-request-validator, que es capaz de validar contenido REST JSON. El proyecto también tiene adaptadores que se integran con varios marcos de pruebas y mocking para ayudar a garantizar que se cumplen las especificaciones de la API como parte de las pruebas. La herramienta tiene un OpenApiInteractionValidator
, que se utiliza para crear un ValidationReport
en un intercambio.
El siguiente código demuestra la construcción de un validador a partir de la especificación, incluyendo cualquier basePathOverrides
-que puede ser necesario si se despliega una API detrás de una infraestructura que altera la ruta. El informe de validación se genera a partir del análisis de la solicitud y la respuesta en el punto en el que se ejecuta la validación :
//Using the location of the specification create an interaction validator
//The base path override is useful if the validator will be used
//behind a gateway/proxy
final
OpenApiInteractionValidator
validator
=
OpenApiInteractionValidator
.
createForSpecificationUrl
(
specUrl
)
.
withBasePathOverride
(
basePathOverride
)
.
build
;
//Requests and Response objects can be converted or created using a builder
final
ValidationReport
report
=
validator
.
validate
(
request
,
response
);
if
(
report
.
hasErrors
())
{
// Capture or process error information
}
Ejemplos y burlas
La OAS de puede proporcionar respuestas de ejemplo para las rutas de la especificación. Los ejemplos, como ya hemos comentado, son útiles para la documentación, para ayudar a los desarrolladores a comprender el comportamiento esperado de la API. Algunos productos han empezado a utilizar ejemplos para permitir al usuario consultar la API y devolver respuestas de ejemplo de un servicio simulado. Esto puede ser realmente útil en funciones como el portal del desarrollador, que permite a los desarrolladores explorar la documentación e invocar las API. Otra función útil de los simulacros y los ejemplos es la posibilidad de compartir ideas entre el productor y el consumidor antes de comprometerse a construir el servicio. Poder "probar" la API suele ser más valioso que intentar revisar si una especificación cumpliría tus requisitos.
Los ejemplos pueden introducir potencialmente un problema interesante, y es que esta parte de la especificación es esencialmente una cadena (para modelar XML/JSON, etc.).openapi-examples-validator valida que un ejemplo coincide con la OEA para la correspondiente solicitud/respuesta component
de la API .
Detectar cambios
Las especificaciones de OpenAPI también pueden ser útiles para detectar cambios en una API. Esto puede ser increíblemente útil como parte de un pipeline DevOps. Detectar cambios para la compatibilidad con versiones anteriores es muy importante, pero primero tenemos que entender el versionado de las API con más detalle.
Versionado de la API
Hemos explorado en las ventajas de compartir una OAS con un consumidor, incluida la velocidad de integración. Considera el caso de que varios consumidores empiecen a operar con la API. ¿Qué ocurre cuando se produce un cambio en la API o uno de los consumidores solicita que se añadan nuevas funciones a la API?
Demos un paso atrás y pensemos si esto fuera una biblioteca de código incorporada a nuestra aplicación en tiempo de compilación. Cualquier cambio en la biblioteca se empaquetaría como una nueva versión y hasta que el código se recompilara y probara con la nueva versión, no habría impacto en las aplicaciones de producción. Como las API son servicios en ejecución, tenemos unas cuantas opciones de actualización que están a nuestra disposición inmediatamente cuando se solicitan cambios:
- Publica una nueva versión e implántala en una nueva ubicación.
-
Las aplicaciones antiguas siguen funcionando con la versión anterior de las API. Esto está bien desde el punto de vista del consumidor, ya que éste sólo se actualiza a la nueva ubicación y API si necesita las nuevas funciones. Sin embargo, el propietario de la API tiene que mantener y gestionar varias versiones de la API, incluidos los parches y la corrección de errores que puedan ser necesarios.
- Publica una nueva versión de la API que sea compatible con la versión anterior de la API.
-
Esto permite realizar cambios aditivos sin afectar a los usuarios existentes de la API. No se requieren cambios por parte del consumidor, pero puede que tengamos que considerar el tiempo de inactividad o la disponibilidad de las versiones antigua y nueva durante la actualización. Si hay una pequeña corrección de errores que cambie algo tan pequeño como un nombre de campo incorrecto, se rompería la compatibilidad.
- Rompe la compatibilidad con la API anterior y todos los consumidores deben actualizar el código para utilizar la nueva API.
-
Esto parece una idea horrible al principio, ya que provocaría que las cosas se rompieran inesperadamente en producción.1 Sin embargo, puede presentarse una situación en la que no podamos evitar romper la compatibilidad con versiones anteriores. Este tipo de cambio puede desencadenar un cambio de bloqueo de todo el sistema que requiera la coordinación del tiempo de inactividad.
El reto es que todas estas opciones de actualización diferentes ofrecen ventajas, pero también inconvenientes, tanto para el consumidor como para el productor. La realidad es que queremos poder admitir una combinación de las tres opciones. Para ello, tenemos que introducir normas en torno al versionado y a cómo se exponen las versiones al consumidor de .
Versionado semántico
El versionadosemántico ofrece a un enfoque que podemos aplicar a las API REST para ofrecernos una combinación de las opciones de actualización anteriores. El versionado semántico define una representación numérica atribuida a una versión de la API. Ese número se basa en el cambio de comportamiento con respecto a la versión anterior, utilizando las siguientes reglas:
-
Una versión principal introduce cambios no compatibles con versiones anteriores de la API. En una plataforma API, la actualización a una nueva versión principal es una decisión activa del consumidor. Es probable que haya una guía de migración y un seguimiento a medida que los consumidores se actualizan a la nueva API.
-
Una versión menor introduce un cambio compatible con la versión anterior de la API. En una plataforma de servicios API, es aceptable que los consumidores reciban versiones menores sin realizar un cambio activo en el lado del cliente.
-
Una versión de parche no cambia ni introduce nuevas funcionalidades, sino que se utiliza para corregir errores en una versión de funcionalidad existente en
Major.Minor
.
El formato para el versionado semántico puede representarse como Major.Minor.Patch
. Por ejemplo, 1.5.1 representaría la versión mayor 1, la versión menor 5, con una actualización de parche de 1. En el capítulo 5 explorarás cómo el versionado semántico conecta con el concepto de ciclo de vida de la API y las versiones.
Especificación y versionado de OpenAPI
Ahora que hemos explorado el versionado, podemos ver ejemplos de cambios que rompen y cambios que no rompen utilizando la especificación de la API de asistentes. Hay varias herramientas entre las que elegir para comparar especificaciones, y en este ejemplo utilizaremos openapi-diff de OpenAPITools.
Empezaremos con un cambio de ruptura: cambiaremos el nombre del campo givenName
porfirstName
. Se trata de un cambio de ruptura porque los consumidores esperarán analizar givenName
, no firstName
. Podemos ejecutar la herramienta diff desde un contenedor docker utilizando el siguiente comando:
$docker run --rm -t \ -v $(pwd):/specs:ro \ openapitools/openapi-diff:latest /specs/original.json /specs/first-name.json ========================================================================== ... - GET /attendees Return Type: - Changed 200 OK Media types: - Changed */* Schema: Broken compatibility Missing property: [n].givenName (string) -------------------------------------------------------------------------- -- Result -- -------------------------------------------------------------------------- API changes broke backward compatibility --------------------------------------------------------------------------
Podemos intentar añadir un nuevo atributo al tipo de retorno /attendees
para añadir un campo adicional llamado age
. Añadir nuevos campos no rompe el comportamiento existente y, por tanto, no rompe la compatibilidad:
$ docker run --rm -t \ -v $(pwd):/specs:ro \ openapitools/openapi-diff:latest --info /specs/original.json /specs/age.json ========================================================================== ... - GET /attendees Return Type: - Changed 200 OK Media types: - Changed */* Schema: Backward compatible -------------------------------------------------------------------------- -- Result -- -------------------------------------------------------------------------- API changes are backward compatible --------------------------------------------------------------------------
Merece la pena probarlo para ver qué cambios serían compatibles y cuáles no. Introducir este tipo de herramientas como parte de la canalización de API va a ayudar a evitar cambios no compatibles inesperados para los consumidores. Las especificaciones OpenAPI son una parte importante de un programa de API, y cuando se combinan con herramientas, versionado y ciclo de vida, tienen un valor incalculable.
Advertencia
Las herramientas suelen ser específicas de la versión de OpenAPI, por lo que es importante comprobar si la herramienta es compatible con la especificación con la que estás trabajando. En el ejemplo anterior probamos la herramienta diff con una versión anterior de una especificación y no se detectaron cambios de ruptura .
Implementar RPC con gRPC
Los servicios este-oeste , como Attendee, suelen tener más tráfico y pueden implementarse como microservicios utilizados en toda la arquitectura. gRPC puede ser una herramienta más adecuada que REST para los servicios este-oeste, debido a la menor transmisión de datos y velocidad dentro del ecosistema. Cualquier decisión sobre el rendimiento debe medirse siempre para estar informado.
Vamos a explorar el uso de un Spring Boot Starter para crear rápidamente un servidor gRPC. El siguiente archivo .proto modela el mismo objeto attendee
que exploramos en nuestro ejemplo de OpenAPI Specification. Al igual que con OpenAPI Specifications, generar código a partir de un esquema es rápido y está soportado en varios idiomas.
El archivo .proto de los asistentes define una solicitud vacía y devuelve una respuesta repetida Attendee
. En los protocolos utilizados para representaciones binarias, es importante tener en cuenta que la posición y el orden de los campos son fundamentales, ya que rigen la disposición del mensaje. Añadir un nuevo servicio o un nuevo método es compatible con versiones anteriores, al igual que añadir un campo a un mensaje, pero hay que tener cuidado. Los nuevos campos que se añadan no deben ser campos obligatorios, ya que, de lo contrario, se rompería la compatibilidad con versiones anteriores.
Eliminar un campo o cambiarle el nombre romperá la compatibilidad, al igual que cambiar el tipo de datos de un campo. Cambiar el número de campo también es un problema, ya que los números de campo se utilizan para identificar los campos en el cable. Las restricciones de codificación con gRPC significan que la definición debe ser muy específica. REST y OpenAPI son bastante indulgentes, ya que la especificación es sólo una guía.2 Los campos adicionales y el orden no importan en OpenAPI, por lo que el versionado y la compatibilidad son aún más importantes cuando se trata de gRPC:
syntax = "proto3"; option java_multiple_files = true; package com.masteringapi.attendees.grpc.server; message AttendeesRequest { } message Attendee { int32 id = 1; string givenName = 2; string surname = 3; string email = 4; } message AttendeeResponse { repeated Attendee attendees = 1; } service AttendeesService { rpc getAttendees(AttendeesRequest) returns (AttendeeResponse); }
El siguiente código Java muestra una estructura sencilla para implementar el comportamiento en las clases de servidor gRPC generadas:
@GrpcService
public
class
AttendeesServiceImpl
extends
AttendeesServiceGrpc
.
AttendeesServiceImplBase
{
@Override
public
void
getAttendees
(
AttendeesRequest
request
,
StreamObserver
<
AttendeeResponse
>
responseObserver
)
{
AttendeeResponse
.
Builder
responseBuilder
=
AttendeeResponse
.
newBuilder
();
//populate response
responseObserver
.
onNext
(
responseBuilder
.
build
());
responseObserver
.
onCompleted
();
}
}
Puedes encontrar el servicio Java que modela este ejemplo en la página GitHub de este libro. gRPC no puede consultarse directamente desde un navegador sin bibliotecas adicionales, aunque puedes instalar gRPC UI para utilizar el navegador para realizar pruebas.grpcurl
también proporciona una herramienta de línea de comandos:
$ grpcurl -plaintext localhost:9090 \ com.masteringapi.attendees.grpc.server.AttendeesService/getAttendees { "attendees": [ { "id": 1, "givenName": "Jim", "surname": "Gough", "email": "gough@mail.com" } ] }
gRPC nos da otra opción para consultar nuestro servicio y define una especificación para que el consumidor genere código. gRPC tiene una especificación más estricta que OpenAPI y requiere que los métodos/internales sean entendidos por el consumidor de .
Modelar los intercambios y elegir un formato de API
En la Introducción, en hablamos del concepto de patrones de tráfico y de la diferencia entre las peticiones procedentes de fuera del ecosistema y las peticiones dentro del ecosistema. Los patrones de tráfico son un factor importante a la hora de determinar el formato adecuado de API para el problema que nos ocupa. Cuando tenemos pleno control sobre los servicios y los intercambios dentro de nuestra arquitectura basada en microservicios, podemos empezar a hacer concesiones que no podríamos hacer con los consumidores externos.
Es importante reconocer que es probable que las características de rendimiento de un servicio este-oeste sean más aplicables que las de un servicio norte-sur. En un intercambio norte-sur, el tráfico que se origina fuera del entorno del productor generalmente implicará el intercambio utilizando Internet. Internet introduce un alto grado de latencia, y una arquitectura de API siempre debe tener en cuenta los efectos combinados de cada servicio. En una arquitectura basada en microservicios, es probable que una solicitud norte-sur implique múltiples intercambios este-oeste. Un alto intercambio de tráfico este-oeste debe ser eficiente para evitar ralentizaciones en cascada que se propaguen alconsumidor.
Servicios de alto tráfico
En nuestro ejemplo, Asistentes es un servicio central. En una arquitectura basada en microservicios, los componentes harán un seguimiento de un attendeeId
. Las API ofrecidas a los consumidores recuperarán potencialmente los datos almacenados en el servicio Asistentes, y a escala será un componente de alto tráfico.
Si la frecuencia de intercambio es alta entre servicios, el coste de la transferencia de red debido al tamaño de la carga útil y a las limitaciones de un protocolo frente a otro será más profundo a medida que aumente el uso. El coste puede presentarse en costes monetarios de cada transferencia o en el tiempo total que tarda el mensaje en llegar al destino.
Grandes cargas de intercambio
Los grandes tamaños de carga útil de también pueden convertirse en un reto en los intercambios de API y son susceptibles de disminuir el rendimiento de la transferencia a través del cable. JSON sobre REST es legible por humanos y a menudo será más verboso que una representación fija o binaria, lo que alimentará un aumento de los tamaños de carga útil.
Consejo
Un error común es que la "legibilidad humana" se cita como razón principal para utilizar JSON en las transferencias de datos. El número de veces que un desarrollador tendrá que leer un mensaje frente a la consideración del rendimiento no es un argumento de peso con las herramientas modernas de rastreo. También es raro que los archivos JSON grandes se lean de principio a fin. Un mejor registro y gestión de errores puede mitigar el argumento de la legibilidad humana.
Otro factor en los intercambios de grandes cargas útiles es el tiempo que tardan los componentes en analizar el contenido del mensaje en objetos de dominio a nivel de lenguaje. El tiempo de rendimiento del análisis sintáctico de los formatos de datos varía enormemente según el lenguaje en el que se implemente un servicio. Muchos lenguajes tradicionales del lado del servidor pueden tener problemas con JSON en comparación con una representación binaria, por ejemplo. Merece la pena explorar el impacto del análisis sintáctico e incluir esa consideración al elegir un formato de intercambio .
Ventajas de rendimiento de HTTP/2
Utilizar Los servicios basados en HTTP/2 pueden ayudar a mejorar el rendimiento de los intercambios al admitir la compresión y el encuadre binario. La capa de encuadre binario es transparente para el desarrollador, pero entre bastidores dividirá y comprimirá el mensaje en trozos más pequeños. La ventaja del encuadre binario es que permite una multiplexación completa de solicitud y respuesta a través de una única conexión. Considera la posibilidad de procesar una lista en otro servicio y el requisito es recuperar 20 asistentes diferentes; si los recuperáramos como solicitudes HTTP/1 individuales, requeriría la sobrecarga de crear 20 nuevas conexiones TCP. La multiplexación nos permite realizar 20 solicitudes individuales a través de una única conexión HTTP/2.
gRPC utiliza HTTP/2 por defecto y reduce el tamaño del intercambio al utilizar un protocolo binario. Si el ancho de banda es una preocupación o un coste, entonces gRPC proporcionará una ventaja, en particular a medida que las cargas útiles de contenido aumenten significativamente de tamaño. gRPC puede ser beneficioso en comparación con REST si el ancho de banda de la carga útil es una preocupación acumulativa o el servicio intercambia grandes volúmenes de datos. Si los grandes volúmenes de intercambio de datos son frecuentes, también vale la pena considerar algunas de las capacidades asíncronas de gRPC.
Consejo
HTTP/3 está en camino y lo cambiará todo. HTTP/3 utiliza QUIC, un protocolo de transporte basado en UDP. Puedes obtener más información en HTTP/3 explicado.
Formatos antiguos
No todos los servicios de una arquitectura se basarán en un diseño moderno. En el Capítulo 8 veremos cómo aislar y hacer evolucionar los componentes antiguos, ya que los componentes antiguos serán una consideración activa para las arquitecturas en evolución. Es importante que quienes participen en una arquitectura de API comprendan el impacto general en el rendimiento de la introducción de componentes antiguos.
Directriz: Modelización de intercambios
Cuando el consumidor es el equipo del sistema de conferencia heredado, el intercambio suele ser una relación este-oeste. Cuando el consumidor es el equipo del PPC, el intercambio suele ser una relación norte-sur. La diferencia en los requisitos de acoplamiento y rendimiento requerirá que los equipos consideren cómo se modela el intercambio. Verás algunos aspectos a considerar en la directriz que se muestra en la Tabla 1-3.
Decisión |
¿Qué formato debemos utilizar para modelar la API de nuestro servicio? |
Puntos de debate |
¿El intercambio es de norte a sur o de este a oeste? ¿Tenemos el control del código del consumidor? ¿Existe un dominio empresarial sólido en varios servicios o queremos permitir que los consumidores construyan sus propias consultas? ¿Qué consideraciones de versionado debemos tener? ¿Cuál es la frecuencia de implementación/cambio del modelo de datos subyacente? ¿Se trata de un servicio de alto tráfico en el que se han planteado problemas de ancho de banda o de rendimiento? |
Recomendaciones |
Si la API es consumida por usuarios externos, REST es una barrera de entrada baja y proporciona un modelo de dominio sólido. Los usuarios externos también suelen significar que es deseable un servicio con acoplamiento suelto y baja dependencia. Si la API interactúa entre dos servicios bajo el estrecho control del productor o se demuestra que el servicio tiene mucho tráfico, considera gRPC. |
Especificaciones múltiples
En este capítulo hemos explorado una variedad de formatos de API a considerar en una arquitectura de API, y quizás la pregunta final sea "¿Podemos proporcionar todos los formatos?"La respuesta es sí, podemos dar soporte a una API que tenga una presentación RESTful, un servicio gRPC y conexiones a un esquema GraphQL. Sin embargo, no va a ser fácil y puede que no sea lo más adecuado. En esta sección final, exploraremos algunas de las opciones disponibles para una API multiformato y los retos que puede presentar.
¿Existe la Especificación Dorada?
El archivo .proto de los asistentes y la Especificación OpenAPI no parecen muy distintos; contienen los mismos campos y ambos tienen tipos de datos. ¿Es posible generar un archivo .proto a partir de una OAS utilizando la herramienta openapi2proto? Si ejecutas openapi2proto --spec spec-v2.json
, el archivo . proto saldrá con los campos ordenados alfabéticamente por defecto. Esto está bien hasta que añadimos un nuevo campo a la OAS que es compatible con versiones anteriores y, de repente, el ID de todos los campos cambia, rompiendo la compatibilidad con versiones anteriores.
El siguiente ejemplo de archivo .proto muestra que la adición de a_new_field
se añadiría alfabéticamente al principio, cambiando el formato binario y rompiendo los servicios existentes:
message Attendee { string a_new_field = 1; string email = 2; string givenName = 3; int32 id = 4; string surname = 5; }
Nota
Existen otras herramientas que resuelven el problema de la conversión de la especificación, pero hay que tener en cuenta que algunas herramientas sólo admiten la versión 2 de la especificación OpenAPI. El tiempo que se tarda en pasar de la versión 2 a la 3 en algunas de las herramientas creadas en torno a OpenAPI ha hecho que muchos productos necesiten admitir ambas versiones de la OAS.
Una opción alternativa de es grpc-gateway, que genera un proxy inverso que proporciona una fachada REST frente al servicio gRPC. El proxy inverso se genera en tiempo de compilación contra el archivo .proto y producirá un mapeo de mejor esfuerzo a REST, similar a openapi2proto
. También puedes suministrar extensiones dentro del archivo . proto para mapear los métodos RPC a una representación agradable en el OAS:
import "google/api/annotations.proto"; //... service AttendeesService { rpc getAttendees(AttendeesRequest) returns (AttendeeResponse) { option(google.api.http) = { get: "/attendees" }; }
Utilizar grpc-gateway nos da otra opción para presentar un servicio REST y gRPC. Sin embargo, grpc-gateway implica varios comandos y una configuración que sólo sería familiar para los desarrolladores que trabajan con el lenguaje Go o el entorno de compilación.
Retos de las especificaciones combinadas
Es importante dar un paso atrás aquí y considerar lo que estamos intentando hacer. Al convertir de OpenAPI, estamos intentando convertir nuestra representación RESTful en una serie de llamadas gRPC. Estamos intentando convertir un modelo de dominio hipermedia ampliado en una llamada de función a función de nivel inferior. Esto es una confusión potencial de la diferencia entre RPC y API y probablemente va a dar lugar a luchas con la compatibilidad.
Con la conversión de gRPC a OpenAPI tenemos un problema similar; el objetivo es intentar tomar gRPC y hacer que parezca una API REST, lo que probablemente creará una serie de problemas difíciles a la hora de hacer evolucionar el servicio.
Una vez que las especificaciones se combinan o se generan unas a partir de otras, el versionado se convierte en un reto. Es importante tener en cuenta cómo las especificaciones gRPC y OpenAPI mantienen sus requisitos individuales de compatibilidad. Debe tomarse una decisión activa sobre si acoplar el dominio REST a un dominio RPC tiene sentido y añade valor global.
En lugar de generar RPC para este-oeste a partir de norte-sur, lo que tiene más sentido es diseñar cuidadosamente la arquitectura basada en microservicios (RPC) de comunicación independientemente de la representación REST, permitiendo que ambas API evolucionen libremente. Ésta es la elección que hemos hecho para el caso práctico de la conferencia y que se registraría como ADR en el proyecto .
Resumen
En este capítulo hemos tratado cómo diseñar, construir y especificar API y las distintas circunstancias en las que puedes elegir REST o gRPC. Es importante recordar que no se trata de REST frente a gRPC, sino de que, dadas las situaciones, cuál es la opción más adecuada para modelar el intercambio. Los puntos clave son:
-
La barrera para construir API basadas en REST y RPC es baja en la mayoría de las tecnologías. Considerar cuidadosamente el diseño y la estructura es una decisión arquitectónica importante.
-
Al elegir entre los modelos REST y RPC, ten en cuenta el Modelo de Madurez de Richardson y el grado de acoplamiento entre el productor y el consumidor.
-
REST es una norma bastante laxa. Al crear API, ajustarse a una norma de API acordada garantiza que tus API sean coherentes y tengan el comportamiento esperado para tus consumidores. Las normas de API también pueden ayudar a cortocircuitar posibles decisiones de diseño que podrían dar lugar a una API incompatible.
-
Las especificaciones OpenAPI son una forma útil de compartir la estructura de las API y automatizar muchas actividades relacionadas con la codificación. Debes seleccionar activamente las características OpenAPI y elegir qué herramientas o funciones de generación se aplicarán a los proyectos.
-
El versionado es un tema importante que añade complejidad para el productor, pero es necesario para facilitar el uso de la API al consumidor. No planificar el versionado en las API expuestas a los consumidores es peligroso. El versionado debe ser una decisión activa en el conjunto de características del producto, y un mecanismo para transmitir el versionado a los consumidores debe formar parte del debate.
-
gRPC funciona increíblemente bien en los intercambios de gran ancho de banda y es una opción ideal para los intercambios este-oeste. Las herramientas para gRPC son potentes y proporcionan otra opción a la hora de modelar los intercambios.
-
Modelar múltiples especificaciones empieza a ser bastante complicado, sobre todo cuando se genera de un tipo de especificación a otro. El versionado complica aún más las cosas, pero es un factor importante para evitar romper los cambios. Los equipos deben pensárselo bien antes de combinar representaciones RPC con representaciones API RESTful, ya que hay diferencias fundamentales en cuanto a uso y control sobre el código consumidor.
El reto de una arquitectura de API es cumplir los requisitos desde la perspectiva empresarial del consumidor, crear una gran experiencia para el desarrollador en torno a las API y evitar problemas inesperados de compatibilidad. En el Capítulo 2 explorarás las pruebas, que son esenciales para garantizar que los servicios cumplen estos objetivos.
Get Dominar la arquitectura API now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.