Diseño de API: por qué es mejor utilizar referencias en lugar de claves para representar relaciones en una API

imagen¡Hola, Habr!



Tenemos la segunda edición muy esperada de Desarrollo web con Node y Express .



Como parte de nuestra investigación sobre este tema, encontramos un artículo conceptual sobre el diseño de una API web a partir de un modelo, donde se utilizan enlaces a recursos en lugar de claves y valores de base de datos. Original: del blog de Google Cloud, bienvenido en cat.





Cuando modelamos información, la pregunta clave es cómo definir la relación y la relación entre dos entidades. Describir patrones observados en el mundo real en términos de entidades y sus relaciones es una idea fundamental que se remonta al menos a la antigua Grecia. También juega un papel fundamental en la industria moderna de TI.



Por ejemplo, en la tecnología de bases de datos relacionales, las relaciones se describen mediante una clave externa , un valor almacenado en una fila de una tabla y que apunta a una fila diferente, ya sea en otra tabla o en la misma tabla.



Es igualmente importante expresar las relaciones en la API. Por ejemplo, en una API minorista, las entidades de información pueden corresponder a clientes, pedidos, entradas de catálogo, carritos de compra, etc. La API de la cuenta bancaria describe a qué cliente pertenece una cuenta determinada, así como a qué cuenta está asociada cada deuda o crédito.



El método más común utilizado por los desarrolladores de API para expresar relaciones es proporcionando claves de base de datos o proxies para ellos en los campos de las entidades asociadas con esas claves. Sin embargo, en al menos una clase de API (orientada a la web) existe una alternativa preferible a este enfoque: el uso de enlaces web.



Según el Grupo de trabajo de ingeniería de Internet ( IETF), un enlace web puede considerarse una herramienta para describir las relaciones entre las páginas de la web. Los enlaces web más famosos son aquellos que aparecen en páginas HTML y están incluidos en elementos de enlace o ancla o en encabezados HTTP. Pero los enlaces también pueden aparecer en los recursos de la API, y usarlos en lugar de claves externas reduce significativamente la cantidad de información que el proveedor de la API tiene que documentar adicionalmente y que el usuario necesita estudiar.



Un enlace es un elemento en un recurso web que contiene una referencia a otro recurso web, así como el nombre de la relación entre los dos recursos. Una referencia a otra entidad se escribe en un formato especial llamado "identificador de recurso único" (URI), para el cual existe un estándar IETF... En este estándar, la palabra "recurso" se refiere a cualquier entidad apuntada por un URI. El nombre del tipo de relación en el vínculo se puede considerar igual que el nombre de la columna de la base de datos que contiene las claves externas, y el URI del vínculo es el mismo que el valor de la clave externa. Los más útiles de todos los URI son aquellos que proporcionan información sobre el recurso referenciado mediante el protocolo web estándar. Estos URI se denominan "Localizador uniforme de recursos" (URL) y la URL más importante para las API es la URL HTTP.



Si bien los enlaces no se utilizan ampliamente en las API, algunas API web muy famosas todavía se basan en URL HTTP como medio para representar relaciones. Estos son, por ejemplo, la API de Google Drivey la API de GitHub . ¿Por que es esto entonces? En este artículo, le mostraré cómo usar la API de claves externas en la práctica, explicaré sus desventajas en comparación con el uso de enlaces y le mostraré cómo convertir un diseño que usa claves externas a uno donde se usan enlaces.



Representar relaciones con claves externas



Considere la popular aplicación educativa de la tienda de mascotas. Esta aplicación almacena registros para rastrear información sobre mascotas y sus dueños. Las mascotas tienen atributos como nombre, especie y raza. Los propietarios tienen nombres y direcciones. Cada mascota está relacionada con su dueño y la relación inversa le permite encontrar todas las mascotas de un dueño en particular.



En un diseño típico basado en claves, la API de la tienda de mascotas proporciona dos recursos que se ven así:







La relación entre Lassie y Joe se expresa así: desde el punto de vista de Lassie, Joe tiene un nombre y un significado que corresponden a "propietario". No se expresa la relación opuesta. El valor del propietario es "98765", que es una clave externa. Esta es probablemente la clave externa real de la base de datos, es decir, estamos tratando con el valor de la clave primaria de alguna fila de alguna tabla de la base de datos. Pero, incluso si la implementación de la API cambia ligeramente los valores clave, aún se acerca a la clave externa en sus características principales.

El valor "98765" no es muy adecuado para el uso directo del cliente. En los casos más comunes, el cliente necesita construir una URL usando este valor y la documentación de la API necesita describir la fórmula para realizar esta conversión. Normalmente, esto se hace definiendo un patrón de URI , como este:



/people/{person_id}







La relación inversa (las mascotas son propiedad del propietario) también se puede exponer a la API mediante la implementación y documentación de uno de los siguientes patrones de URI (las diferencias son solo estilísticas, no sustantivo):



/pets?owner={person_id}

/people/{person_id}/pets








Las API diseñadas de esta manera generalmente requieren que se definan y documenten muchos patrones de URI. El lenguaje más popular para definir tales patrones no es el especificado en la especificación IETF, sino OpenAPI (anteriormente conocido como Swagger). Antes de la versión 3.0, OpenAPI no tenía una forma de especificar qué valores de campo se podían insertar en qué plantillas, por lo que parte de la documentación tenía que escribirse en lenguaje natural y el cliente tenía que adivinar otra. OpenAPI 3.0 introduce una nueva sintaxis llamada "enlaces" para abordar este problema, pero se necesita algo de trabajo para utilizar esta función de forma coherente.



Por lo tanto, tan común como es este estilo, requiere que el proveedor documente y que el cliente aprenda y use una cantidad significativa de patrones de URI que no están bien documentados en las especificaciones actuales de la API. Afortunadamente, existe una opción mejor.



Representar relaciones mediante enlaces



¿Qué pasa si los recursos que se muestran arriba se modificaran de la siguiente manera?







La principal diferencia es que en este caso los valores se expresan usando referencias, no usando valores de clave externa. Aquí los enlaces están escritos en JSON normal, en el formato de pares de nombre / valor (hay una sección a continuación que analiza otros enfoques para escribir enlaces en JSON).



Tenga en cuenta que la relación inversa, es decir, de mascota a propietario, ahora también se implementa explícitamente porque Joel



se ha agregado un campo a la vista "pets"



.



El cambio "id"



a no "self"



es esencialmente necesario ni importante, pero existe un acuerdo de que usar "self"



identifica un recurso cuyos atributos y relaciones se especifican mediante otros pares de nombre / valor en el mismo objeto JSON. "self"



Es el nombre registrado en la IANA para este fin.



Desde el punto de vista de la implementación, reemplazar todas las claves de la base de datos con enlaces debería ser bastante simple: el servidor convierte todas las claves externas de la base de datos en URL, por lo que no es necesario hacer nada en el cliente, pero la API en sí en este caso se simplifica enormemente. y la conectividad entre el cliente y el servidor se cae. Muchos patrones de URI que eran importantes en el primer diseño ya no son necesarios y pueden eliminarse de la especificación y documentación de la API.



Ahora nada impide que el servidor cambie el formato de las nuevas URL en cualquier momento sin afectar a los clientes (por supuesto, el servidor debe seguir cumpliendo con todas las URL formuladas anteriormente). La URL que el servidor pasa al cliente deberá incluir la clave principal de la entidad especificada en la base de datos más información de enrutamiento. Pero, dado que el cliente simplemente repite la URL mientras responde al servidor, y el cliente nunca tiene que analizar la URL, los clientes no necesitan saber cómo formatear la URL. Como resultado, hay menos conectividad entre el cliente y el servidor. El servidor puede incluso recurrir a ocultar sus propias URL usando codificaciones base64 o similares si quiere enfatizar a los clientes que no deben "adivinar" cuál es el formato de la URL, o inferir el significado de las URL a partir de su formato.



En el ejemplo anterior, utilicé la notación URI relativa de referencias, por ejemplo /people/98765



. Quizás el cliente se sentiría un poco más cómodo (aunque el autor no fue muy útil para dar formato a esta publicación) si expresara el URI en forma absoluta, por ejemplo. pets.org/people/98765... Los clientes solo necesitan conocer las reglas de URI estándar definidas en las especificaciones de IETF para convertir dichos URI de un formulario a otro, por lo que elegir un formulario específico para URI no es tan importante como podría pensar. Compare esta situación con la conversión anterior de clave externa a URL, que requería un conocimiento específico de la API de la tienda de mascotas. Las URL relativas son algo más convenientes para los implementadores de servidores, como se explica a continuación, pero las URL absolutas son quizás más convenientes para la mayoría de los clientes. Probablemente esta sea la razón por la que las API de Google Drive y GitHub usan URL absolutas.



En resumen, el uso de enlaces en lugar de claves externas para expresar relaciones entre API reduce la cantidad de información que un cliente necesita saber para interactuar con la API y también reduce la cantidad de conectividad que puede ocurrir entre clientes y servidores.



Rocas submarinas



Aquí hay algunas cosas a considerar antes de continuar con el uso de enlaces.



Muchas implementaciones de API se han proporcionado con proxies inversos para seguridad, equilibrio de carga y más. A algunos proxies les gusta reescribir URL. Cuando la API usa claves externas para representar relaciones, la única URL que debe reescribirse en el proxy es la URL de solicitud principal. En HTTP, esta URL se divide entre la barra de direcciones (la primera línea del encabezado) y el encabezado del host.



Una API que utiliza enlaces para expresar relaciones tendrá otras URL en los encabezados y el cuerpo tanto de la solicitud como de la respuesta, y estas URL también deberán reescribirse. Hay varias formas diferentes de lidiar con esto:



  1. URL . URL, .
  2. , . , , , -, , .
  3. . URL; , URL , -. URL, , , , , . - (, URL, «» «»), . , URL, , URL , , , URL .


Las URL relativas sin barras inclinadas también son más difíciles de usar para los clientes porque tienen que trabajar con una biblioteca estandarizada en lugar de una simple concatenación de cadenas para manejar esas URL y comprender y almacenar cuidadosamente la URL base.



El uso de una biblioteca estandarizada para manejar URL es una buena práctica para los clientes de todos modos, pero muchos clientes no lo hacen.

Al usar enlaces, es posible que también deba verificar el control de versiones de su API. En muchas API, es habitual poner números de versión en la URL, así:



/v1/pets/12345

/v2/pets/12345

/v1/people/98765

/v2/people/98765








Este es el tipo de control de versiones, en el que los datos de un recurso en particular se pueden ver simultáneamente en más de un “formato”; no se trata de versiones que se reemplazan entre sí con el tiempo a medida que se editan posteriormente.



Esta situación es muy similar a la capacidad de ver el mismo documento en varios lenguajes naturales, para lo cual existe un estándar web.; qué lástima que no exista tal estándar para las versiones. Al asignar a cada versión su propia URL, actualiza cada versión a un recurso web completamente funcional. No hay nada de malo en las "URL versionadas" de este tipo, pero no son adecuadas para expresar enlaces. Si el cliente solicita a Lassie en formato versión 2, esto no significa que también quiera recibir en formato 2 información sobre Joe, el propietario de Lassie, por lo que el servidor no puede elegir qué número de versión incluir en el enlace.



Quizás ni siquiera se proporcione el formato 2 para describir a los propietarios. Tampoco tiene sentido conceptual usar una versión específica de la URL en sus enlaces; después de todo, Lassie no pertenece a una versión específica de Joe, sino al propio Joe. Por lo tanto, incluso si proporciona una URL en el formato / v1 / people / 98765 y, por lo tanto, identifica una versión específica de Joe, también debe proporcionar la URL / people / 98765 para identificar al propio Joe, y es la segunda opción que utiliza. en enlaces. Otra opción es definir solo la URL / people / 98765 y permitir que los clientes seleccionen una versión específica incluyendo el encabezado de solicitud para eso. No hay un estándar para este encabezado, pero si lo llama Accept-Version, funciona bien con los nombres de los encabezados estándar.Personalmente, prefiero usar un encabezado para el control de versiones y evitar usar números de versión en la URL. pero las URL con números de versión son populares y, a menudo, también implemento el título. y "URL versionadas", ya que es más fácil implementar ambas que discutir cuál es mejor. Puede leer más sobre el control de versiones de API en este artículo .



Es posible que deba documentar algunos patrones de URL de todos modos.



En la mayoría de las API web, el servidor asigna una nueva URL de recurso cuando se crea un nuevo recurso mediante el método POST. Si usa este método para crear recursos y especificar relaciones mediante vínculos, no es necesario que publique una plantilla para los URI de esos recursos. Sin embargo, algunas API permiten al cliente controlar la URL del nuevo recurso. Al permitir que los clientes controlen las URL de nuevos recursos, simplificamos en gran medida muchos de los patrones de secuencias de comandos de API para desarrolladores de aplicaciones para el usuario, y también admitimos secuencias de comandos en las que la API se utiliza para sincronizar el modelo de información con una fuente de información externa. HTTP proporciona un método especial para este propósito: PUT. PUT significa "crear un recurso en esta URL si aún no existe, y si existe, actualizarlo".Si su API permite a los clientes crear nuevas entidades usando el método PUT, entonces debe documentar las reglas para construir nuevas URL, quizás incluyendo el patrón URI en la especificación de la API. También puede otorgar a los clientes un control parcial sobre la URL al incluir un valor similar a una clave principal en el cuerpo o en los encabezados POST. En este caso, el patrón POST URI no es necesario per se, pero el cliente aún tendrá que aprender el patrón URI para aprovechar al máximo la predictibilidad URI resultante.sin embargo, el cliente aún tendrá que aprender el patrón URI para aprovechar al máximo la previsibilidad resultante del URI.sin embargo, el cliente aún tendrá que aprender el patrón URI para aprovechar al máximo la previsibilidad resultante del URI.



Otro contexto en el que es apropiado documentar los patrones de URL es cuando la API permite a los clientes codificar solicitudes de URL. No todas las API le permiten solicitar sus recursos, pero esto puede ser muy útil para los clientes y, naturalmente, permitir que los clientes codifiquen las solicitudes de URL y recuperen los resultados utilizando un método GET. El siguiente ejemplo muestra por qué.



En el ejemplo anterior, hemos incluido el siguiente par de nombre / valor en la vista de Joe:



"pets": "/pets?owner=/people/98765"







El cliente no necesita saber nada sobre su estructura para utilizar esta URL, aparte de que fue escrita de acuerdo con las especificaciones estándar. Por lo tanto, el cliente puede obtener una lista de las mascotas de Joe desde este enlace sin tener que aprender ningún lenguaje de consulta. Tampoco es necesario documentar los formatos de su URL en la API, pero solo si el cliente primero realiza una solicitud GET a /people/98765



... Si, además, la capacidad de realizar solicitudes está documentada en la API de la tienda de mascotas, entonces el cliente puede componer la misma URL de solicitud o una equivalente para recuperar las mascotas del propietario de interés, sin primero extraer al propietario en sí mismo, será suficiente para conocer el URI del propietario. Quizás aún más importante, el cliente también puede generar solicitudes como las siguientes, que de otro modo no serían posibles: La especificación URI describe para este propósito una parte de la URL HTTP denominada " componente de solicitud



/pets?owner=/people/98765&species=Dog

/pets?species=Dog&breed=Collie








"¿La parte de la URL después del primer"? " al primer "#". El estilo de solicitud de URI que prefiero usar es poner siempre solicitudes específicas del cliente en el componente de solicitud de URI, pero también es aceptable expresar solicitudes de cliente en la parte de la URL llamada "ruta . ”De todos modos, debe decirles a los clientes cómo se construyen estas URL; en realidad, está diseñando y documentando el lenguaje de solicitud específico de su API. Por supuesto, también puede permitir que los clientes coloquen solicitudes en el cuerpo del mensaje, no en el URL, y use el método POST en lugar de GET. Límite práctico en el tamaño de la URL - por encima de 4k bytes siempre se siente tentado - se recomienda admitir POST para solicitudes incluso si ya admite GET.



Debido a que las consultas son una característica tan útil en las API, y debido a que los lenguajes de consulta no son fáciles de diseñar e implementar, han surgido tecnologías como GraphQL . Nunca he usado GraphQL, por lo que no puedo recomendarlo, pero puede considerarlo como una alternativa para implementar la capacidad de consulta en su API. Las herramientas de solicitud de API, incluido GraphQL, se utilizan mejor como un complemento de la API HTTP estándar para leer y escribir recursos, en lugar de como una alternativa a HTTP.



Y por cierto ... ¿Cuál es la mejor forma de escribir enlaces en JSON?



JSON, a diferencia de HTML, no tiene un mecanismo integrado para expresar enlaces. Mucha gente tiene su propia forma de entender cómo se deben expresar los enlaces en JSON, y algunas de esas opiniones se han publicado en documentos más o menos oficiales, pero en la actualidad no existen estándares ratificados por organizaciones de renombre que regulen esto. En el ejemplo anterior, expresé enlaces usando pares de nombre / valor regulares escritos en JSON; prefiero este estilo y, por cierto, este estilo se usa en Google Drive y GitHub. Otro estilo que probablemente verá es este:



  {"self": "/pets/12345",
 "name": "Lassie",
 "links": [
   {"rel": "owner" ,
    "href": "/people/98765"
   }
 ]
}
      
      





Personalmente, no veo para qué sirve este estilo, pero algunas de sus variaciones son bastante populares.



Hay otro estilo de referencia JSON que me gusta, y se ve así:



 {"self": "/pets/12345",
 "name": "Lassie",
 "owner": {"self": "/people/98765"}
}
      
      





El beneficio de este estilo es que proporciona explícitamente: "/people/98765"



es una URL, no solo una cadena. Aprendí este patrón de RDF / JSON . Una de las razones para dominar este patrón es que debe usarlo de todos modos, siempre que desee mostrar información sobre un recurso anidado en otro recurso, como se muestra en el siguiente ejemplo. Si usa este patrón en todas partes, su código obtiene una buena uniformidad:



{"self": "/pets?owner=/people/98765",
 "type": "Collection",
  "contents": [
   {"self": "/pets/12345",
    "name": "Lassie",
    "owner": {"self": "/people/98765"}
   }
 ]
}
      
      





Para obtener más información sobre la mejor manera de usar JSON para representar datos, consulte JSON terriblemente simple .



Finalmente, ¿cuál es la diferencia entre un atributo y una relación?



Creo que la mayoría de los lectores estarán de acuerdo en que JSON carece de un mecanismo incorporado para expresar enlaces, pero también hay una forma de interpretar JSON que le permite argumentar lo contrario. Considere el siguiente JSON:



{"self": "/people/98765",
 "shoeSize": 10
}

      
      





Generalmente se acepta que shoeSize



es un atributo, no una relación, y 10 es un valor, no una entidad. Es cierto que no es menos lógico afirmar que la cadena "10" es en realidad una referencia escrita en una notación especial destinada a hacer referencia a números, hasta el número 11, que es en sí mismo una entidad. Si el undécimo entero es una entidad perfectamente válida y la cadena '10'



solo apunta a él, entonces el par nombre / valor es '"shoeSize": 10'



conceptualmente una referencia, aunque aquí no se utilizan URI.



Lo mismo puede decirse de los valores booleanos y las cadenas, por lo que todos los pares de nombre / valor en JSON pueden tratarse como referencias. Si cree que esta forma de pensar sobre JSON, entonces es natural usar pares simples de nombre / valor en JSON como referencias a entidades a las que también se puede apuntar mediante una URL.



De manera más general, este argumento se formula como "no hay una diferencia fundamental entre atributos y relaciones". Los atributos son simplemente relaciones entre una entidad u otra entidad abstracta o concreta, como un número o un color. Pero históricamente, su procesamiento fue tratado de una manera especial. Francamente, esta es una versión bastante abstracta de la percepción del mundo. Entonces, si le muestras a alguien un gato negro y le preguntas cuántos objetos hay, la mayoría de la gente te dirá que solo hay uno. Pocos dirían que ven dos objetos, un gato y su color negro, y la relación entre ellos.



Los enlaces son simplemente mejores



Las API web que pasan claves de base de datos en lugar de solo enlaces son más difíciles de aprender y también más difíciles de usar para los clientes. Además, las API del primer tipo vinculan más estrechamente al cliente y al servidor, lo que requiere información más detallada como "denominador común", y toda esta información debe documentarse y leerse. La única ventaja de las API del primer tipo es que son tan ubicuas que los programadores se sienten cómodos con ellas, saben cómo crearlas y cómo consumirlas. Si está buscando proporcionar a los clientes API de alta calidad que no requieran toneladas de documentación y maximicen la independencia del cliente del servidor, considere proporcionar enlaces a sus API web en lugar de claves de base de datos.



All Articles