tutorial servers example consumes change basepath rest versioning api-versioning

rest - servers - swagger change base url



¿Cómo gestionas la base de código subyacente para una API versionada? (3)

He estado leyendo sobre estrategias de versiones para las API de ReST, y algo que ninguno de ellos parece abordar es cómo administra la base de código subyacente.

Digamos que estamos haciendo un montón de cambios importantes en una API, por ejemplo, cambiando nuestro recurso de Cliente para que devuelva campos separados de forename y surname lugar de un solo campo de name . (Para este ejemplo, usaré la solución de versiones de URL, ya que es fácil entender los conceptos involucrados, pero la pregunta es igualmente aplicable a la negociación de contenido o encabezados HTTP personalizados)

Ahora tenemos un punto final en http://api.mycompany.com/v1/customers/{id} , y otro punto final incompatible en http://api.mycompany.com/v2/customers/{id} . Todavía estamos lanzando correcciones de errores y actualizaciones de seguridad para la API v1, pero el desarrollo de nuevas características ahora se está centrando en la v2. ¿Cómo escribimos, probamos e implementamos cambios en nuestro servidor API? Puedo ver al menos dos soluciones:

  • Use una rama / etiqueta de control de origen para la base de código v1. v1 y v2 se desarrollan y se implementan de forma independiente, con fusiones de control de revisión que se usan según sea necesario para aplicar la misma corrección de errores a ambas versiones, de forma similar a cómo administraría las bases de código para aplicaciones nativas al desarrollar una nueva versión importante mientras aún admite la versión anterior.

  • Haga que la base de código sea consciente de las versiones de API, de modo que termine con una base de código única que incluya tanto la representación del cliente v1 como la representación del cliente v2. Trate el control de versiones como parte de la arquitectura de su solución en lugar de un problema de implementación, probablemente utilizando alguna combinación de espacios de nombres y enrutamiento para asegurarse de que las solicitudes sean manejadas por la versión correcta.

La ventaja obvia del modelo de sucursal es que es trivial eliminar las versiones antiguas de API, simplemente deje de implementar la rama / etiqueta adecuada, pero si está ejecutando varias versiones, podría terminar con una estructura de sucursal realmente complicada y una tubería de implementación. El modelo de "base de código unificada" evita este problema, pero (¿creo?) Haría que sea mucho más difícil eliminar recursos y puntos finales obsoletos de la base de código cuando ya no sean necesarios. Sé que esto es probablemente subjetivo, ya que es poco probable que haya una respuesta simple y correcta, pero tengo curiosidad por entender cómo las organizaciones que mantienen API complejas en varias versiones están resolviendo este problema.


He usado las dos estrategias que mencionas. De esos dos, estoy a favor del segundo enfoque, que es más simple, en casos de uso que lo respaldan. Es decir, si las necesidades de versiones son simples, entonces vaya con un diseño de software más simple:

  • Un bajo número de cambios, cambios de baja complejidad o un horario de cambios de baja frecuencia
  • Cambios que son en gran medida ortogonales al resto de la base de código: la API pública puede existir pacíficamente con el resto de la pila sin requerir una ramificación "excesiva" (para cualquier definición del término que elija adoptar) en el código

No encontré demasiado difícil eliminar versiones obsoletas usando este modelo:

  • Una buena cobertura de prueba significó que extraer una API retirada y el código de respaldo asociado aseguraron que no haya regresiones (bueno, mínimas)
  • Una buena estrategia de nomenclatura (nombres de paquetes con versiones API, o versiones API algo más feas en los nombres de métodos) facilitó la localización del código relevante
  • Las preocupaciones transversales son más difíciles; Las modificaciones a los sistemas centrales de back-end para soportar múltiples API deben sopesarse con mucho cuidado. En algún momento, el costo de versionar el backend (ver comentario sobre "excesivo" arriba) supera el beneficio de una única base de código.

El primer enfoque es ciertamente más simple desde el punto de vista de reducir el conflicto entre versiones coexistentes, pero la sobrecarga de mantener sistemas separados tendió a superar el beneficio de reducir el conflicto de versiones. Dicho esto, era muy simple poner de pie una nueva pila de API pública y comenzar a iterar en una rama de API separada. Por supuesto, la pérdida generacional se produjo casi de inmediato, y las ramas se convirtieron en un desastre de fusiones, fusiones de resolución de conflictos y otras cosas divertidas.

Un tercer enfoque se encuentra en la capa arquitectónica: adopte una variante del patrón Facade y abstraiga sus API en capas versionadas de cara al público que se comuniquen con la instancia Facade apropiada, que a su vez se comunica con el backend a través de su propio conjunto de API. Su Fachada (utilicé un adaptador en mi proyecto anterior) se convierte en su propio paquete, autónomo y comprobable, y le permite migrar las API frontend independientemente del backend y entre sí.

Esto funcionará si sus versiones de API tienden a exponer los mismos tipos de recursos, pero con diferentes representaciones estructurales, como en su ejemplo de nombre completo / nombre de pila / apellido. Se vuelve un poco más difícil si comienzan a depender de diferentes cálculos de back-end, como en "Mi servicio de back-end ha devuelto un interés compuesto calculado incorrectamente que se ha expuesto en la API pública v1. Nuestros clientes ya han parcheado este comportamiento incorrecto. Por lo tanto, no puedo actualizar eso cálculo en el backend y hacer que se aplique hasta v2. Por lo tanto, ahora necesitamos bifurcar nuestro código de cálculo de intereses ". Afortunadamente, estos tienden a ser poco frecuentes: en términos prácticos, los consumidores de API RESTful prefieren representaciones de recursos precisas sobre la compatibilidad con errores, incluso entre cambios sin interrupciones en un recurso GET teóricamente idempotente.

Estaré interesado en escuchar su eventual decisión.


La ramificación me parece mucho mejor, y utilicé este enfoque en mi caso.

Sí, como ya mencionó: la corrección de errores en el backport requerirá un poco de esfuerzo, pero al mismo tiempo admitir múltiples versiones bajo una base de origen (con enrutamiento y todas las demás cosas) requerirá, si no menos, pero al menos el mismo esfuerzo, haciendo que el sistema sea más complicado y monstruoso con diferentes ramas de la lógica en el interior (en algún momento de la versión, definitivamente llegará a un gran case() apunta a módulos de versión que tienen código duplicado, o incluso peor if(version == 2) then... ). Además, no olvide que, para fines de regresión, aún debe mantener las pruebas ramificadas.

Con respecto a la política de versiones: mantendría como máximo -2 versiones de las actuales, despreciando el soporte para las antiguas, lo que daría cierta motivación para que los usuarios se muevan.


Para mí, el segundo enfoque es mejor. Lo he usado para los servicios web SOAP y planeo usarlo también para REST.

Mientras escribe, la base de código debe tener en cuenta la versión, pero se puede usar una capa de compatibilidad como capa separada. En su ejemplo, la base de código puede producir representación de recursos (JSON o XML) con nombre y apellido, pero la capa de compatibilidad lo cambiará para que solo tenga un nombre.

El código base debe implementar solo la última versión, digamos v3. La capa de compatibilidad debe convertir las solicitudes y la respuesta entre la versión más reciente v3 y las versiones compatibles, por ejemplo, v1 y v2. La capa de compatibilidad puede tener adaptadores separados para cada versión compatible que se pueden conectar como cadena.

Por ejemplo:

Solicitud de cliente v1: v1 adaptarse a v2 ---> v2 adaptarse a v3 ----> base de código

Solicitud de cliente v2: v1 adaptarse a v2 (omitir) ---> v2 adaptarse a v3 ----> codebase

Para la respuesta, los adaptadores funcionan simplemente en la dirección opuesta. Si está utilizando Java EE, puede usar la cadena de filtro de servlet como cadena de adaptador, por ejemplo.

Eliminar una versión es fácil, elimine el adaptador correspondiente y el código de prueba.