Haré esta pregunta de esta manera: ¿cuáles son las preocupaciones de ingeniería de software para no implementar mi API REST de la manera "correcta"?
¿Qué quieres decir con la forma "correcta"? Bueno, permítame explicar mi percepción de la manera correcta, luego le diré cómo lo estoy haciendo (también, suponga que estoy hablando de una API JSON REST).
La direccion correcta
Apatridia. Esta es la parte que sí entiendo. El cliente mantiene el estado siempre el 100% del tiempo para siempre. No es el trabajo del servidor, es del cliente.
Las acciones esperadas y la respuesta para cada verbo:
- GET : obtiene un recurso especificado en su totalidad, solo limitado por la autorización en la solicitud o por un parámetro de consulta. Esto asegura que no se modifique ningún recurso en el proceso.
- POST : dada una descripción completa del recurso (como un objeto JSON), crea un recurso, luego devuelve ese recurso, con cualquier propiedad del servidor también creada, como fechas o ID.
- BORRAR : elimina un recurso especificado, dando solo una especie de 200 OK como respuesta
- PUT : dada una declaración de objeto completa como entrada, actualiza el recurso en una ubicación específica, actualizando todos los campos del recurso a cada uno de los campos dados en la entrada. Para que quede claro, esto espera que todo el objeto se pase como entrada. Se devuelve todo el recurso actualizado, con todos los campos (según la autorización o cualquier otro indicador de entrada).
- PARCHE : dado que solo los campos que se desean modificar para un recurso, se actualizan solo los campos de un recurso específico que se proporcionan como entrada. (Aquí es donde no estoy claro): ¿Se devuelve todo el recurso? (¿O son solo los campos actualizados? No sé. No me importa).
- Rutas de recursos. Dada la relación de los recursos entre sí, una ruta de recursos puede verse como una de:
- / parentresource /: id
- / parentresource /: id / childresource
- / parentresource /: id / childresource /: childId
- / parentresource /: id / childresource /: childId / subresource /: subresourceId (en este ejemplo, un subrecurso pertenece a un childresource, que pertenece a un recurso primario).
La forma en que quiero hacerlo
Lo anterior es mi comprensión de cómo se supone que funciona una API REST. Ahora déjame enumerar algunas de mis variaciones a lo anterior:
- PUT / PATCH - ¿Cuál es el punto de pasar todo el recurso para su modificación? Solo uso PUT para modificar recursos, y solo paso en los campos que quiero actualizar. Como resultado, no necesito usar PATCH
Rutas de recursos: utilizo GUID en mi aplicación. Como resultado, serán globalmente únicos. ¿Por qué necesito la ruta completa de los recursos, incluidos los recursos principales, si solo puedo referirme a un subrecurso por sí solo? Como:
/ subresource /: subresourceId
Si tuviera que hacerlo de la manera "correcta", tratar de hacer referencia a la subresource requeriría una ruta completa como:
/ parentresource /: id / childresource /: childId / subresource /: subresourceId
Es todo lo necesario ? Porque ahora tengo que tener un manejo de errores adicional si mi ruta contiene un: subresourceId que en realidad no es propiedad de un dado: childId, y lo mismo para un: childId que no es propiedad de un padre: id. Mi servidor se encarga de la autorización de recursos. ¿No puedo hacer referencia al recurso en sí, en lugar de la ruta completa?La respuesta de regreso. Digamos, por ejemplo, que mi estructura de datos es un árbol jerárquico, sin límites prácticos en la profundidad del árbol. Los recursos se encuentran en diferentes niveles en el árbol, de forma jerárquica.
- El GET es obvio. Si obtengo este árbol completo, espero el árbol completo como respuesta, con los recursos contenidos dentro de los recursos.
- Si POST para crear un nuevo recurso, PONER para actualizar o ELIMINAR para eliminar, quiero ver los deltas en el árbol, en lugar de solo ver el recurso que creé / actualicé / eliminé. No quiero tener que volver a llamar al GET del árbol principal después de cada POST, PUT o DELETE, especialmente si hay pequeños cambios en el árbol y solo quiero ver los deltas.
Espero que mis preguntas sean claras.
Si tuviera que ver una implementación REST como la describí, ¿lo miraría y me hablaría de sus inquietudes de ingeniería de software? Si es así, ¿cuáles serían?
Respuestas:
La respuesta subyacente general aquí es que sus ideas pueden funcionar a nivel técnico, pero eso no significa que se ajusten a las convenciones estandarizadas de REST.
Su idea funciona a nivel técnico, pero simplemente no es cómo se ha descrito REST. Tenga en cuenta que cualquier discusión sobre el código de trabajo (es decir, sin errores de compilación o tiempo de ejecución) siempre será una cuestión de convención , no necesariamente de clara superioridad técnica.
Hay muchos matices en la forma en que definimos las entidades "hijo / padre". Más comúnmente, se refiere a una relación de uno a muchos (padres a hijos).
Sin embargo, sospecho que para REST, parte de lo que hace que un niño sea un niño es que existe la expectativa de poder acceder a ellos solo a través del padre, que no tienen su propio identificador único global (y conocido externamente).
Sospecho que esto sigue la misma filosofía (pero no necesariamente por la misma razón) que la de los agregados (y sus raíces) en el Desarrollo impulsado por dominio .
En su caso, lo que llama el "padre" funciona como la raíz agregada. El único punto de contacto (si lo desea) para las llamadas externas.
Es posible que desee concluir de esto que su hijo es en realidad un agregado diferente. Ese puede ser el caso, pero quiero emitir una advertencia con esa decisión. No debe basar su arquitectura en el tipo particular de un campo. No tiene forma de saber si siempre seguirá usando ID únicos a nivel mundial para todas sus entidades. Si eso cambia, por cualquier razón, comprometerá la viabilidad de su arquitectura REST; ya que puede terminar en una situación en la que el niño ya no es identificable de manera única y, por lo tanto, debe ser referenciado a través de su padre.
Estás violando el orden de operaciones del diseño. Una API REST está diseñada específicamente para ser independiente del consumidor. La API no debe construirse de acuerdo con las especificaciones de uno de sus consumidores.
Cuando dices "Quiero ver los deltas en el árbol", lo que realmente estás diciendo es "la aplicación que consume solo necesita ver los deltas en el árbol". Pero eso no le importa a la API REST. Simplemente proporciona un enfoque estandarizado.
Es la naturaleza de los enfoques estandarizados que a menudo carecen de herramientas altamente personalizables y, en cambio, favorecen las herramientas más utilizadas .
¿Puedes desviarte del camino? Bueno, funcionará a nivel técnico. Pero ya no será puro REST. Esto es algo altamente contextual y debe sopesar las opciones.
fuente
La principal, en mi opinión, es la capacidad de delegar el trabajo a componentes genéricos que solo conocen los estándares, no su caso de negocio específico.
Si se está adhiriendo a la interfaz uniforme, es más fácil para otras partes crear componentes que se integren bien con los suyos.
Aquí está Fielding escribiendo en 2008
Una de las formas en que manejamos la "larga vida" es tener un estándar claro que describa la semántica de los mensajes que transmitimos. Si todos están de acuerdo con lo que
PUT
significa, entonces los consumidores y productores de esas solicitudes pueden desarrollarse de manera independiente, y los componentes intermedios entre los dos pueden tomar medidas sensatas sin necesidad de conocer los detalles del mensaje en su contexto específico.¿Cuál es el punto de usar
PUT
entonces?Esa es una línea de solicitud perfectamente válida para un mensaje HTTP, y el cambio en la semántica no confundirá a nadie.
Equivalentemente
Lo cual tiene una semántica sin restricciones; el servidor puede implementar el manejo de esa solicitud de la forma que desee.
Su renuencia a usar PATCH es especialmente extraña en este caso, ya que ya hay estándares propuestos para JSON Patch y JSON Merge Patch : el trabajo de estandarizar un formato de documento de parche ya puede hacerse por usted.
Otra alternativa válida sería tratar el documento de parche como un recurso separado. Semánticamente, podrías imaginar algo como
Eso le brinda una semántica de mensajes honesta y uniforme, sacrificando la invalidación de caché estandarizada .
En una configuración de revisión de código, rechazaría un cambio propuesto que intentara redefinir la semántica de PUT.
La misma consideración es válida también para
PUT
; Si su implementación de PUT se desvía de la semántica estandarizada, entonces su implementación es responsable del daño resultante.Eso está perfectamente bien. A REST no le importa qué ortografía use para sus identificadores de recursos.
Considere la página de inicio de google. ¿Necesita prestar atención a la ortografía del URI para el doodle de hoy? o donde se envía el formulario de búsqueda? No claro que no. La carga útil de HTML incluye URI, y los clientes solo usan los identificadores que se proporcionan, de manera estándar, sin necesidad de analizar esos identificadores.
La información codificada en un URI queda a discreción del servidor de origen, para sus propios fines.
Desalentaría el uso de un URI como punto de entrada de su API.
https://www.example.org/df8f5f87-15ff-4212-8fb8-4fbca2c7efcf
Es un poco incómodo para el consumo humano. Un URI legible por humanos que redirige al recurso UUID estaría bien, un URI legible por humanos que devuelve el contenido del recurso UUID sería mejor.Eso está bien, de nuevo, mira el estándar .
En algunos casos, tiene sentido enviar la nueva representación del recurso como parte de la respuesta (para ahorrarle al cliente la latencia de una solicitud / respuesta GET).
fuente
What's the point in using PUT then?
-- En efecto. Un POST funcionaría perfectamente bien.PATCH
? La definición se ajusta mejor.REST es un estilo arquitectónico para manipular recursos de manera apátrida (donde apátrida significa que cada manipulación se sostiene por sí misma y no depende de otras manipulaciones que puedan haber tenido lugar).
El uso de los verbos PUT / PATCH / POST / GET / DELETE proviene del uso común del protocolo HTTP utilizado para transferir y manipular los recursos. El significado de esos verbos está definido por un estándar de Internet ( RFC7231 ).
Teniendo en cuenta estos antecedentes, su uso de PUT no es estándar y puede confundir a otros desarrolladores que quieran usar su API.
Con respecto a las rutas de recursos, casi nadie se preocupa por su ortografía exacta (incluso si un recurso secundario aparece como secundario). Lo que le importa a la gente es que cada recurso esté identificado de manera única. El sistema
/parent/:pid/child/:cid
se usa a menudo cuando las identificaciones secundarias solo son únicas dentro de un padre para tener una ruta global única al recurso.fuente
Me parece que realmente deberías estar usando PATCH como la semántica aquí.
PUT explica el estado exacto deseado. Esto es útil cuando un recurso puede estar cambiando con frecuencia, y el cambio deseado debe ocurrir dentro de un contexto específico.
PATCH explica un delta deseado a lo que sea que esté allí. Esto es útil cuando al cambio no le importa el contexto, o el contexto relevante es mucho más pequeño que todo el recurso.
Una imagen es un buen ejemplo en el que tendría sentido simplemente cargar todo. Es importante que todo el recurso se comunique para garantizar un contexto relevante.
Por el contrario, actualizar el recuento de reproducción en una lista de reproducción de música podría tener más sentido para ser un delta. No porque sea un pequeño cambio, sino porque volver a enviar la lista completa podría deshacer fácilmente los cambios en el contenido de la lista.
No, realmente no necesitas usar rutas, nunca. Digamos que mantiene todos sus archivos en el escritorio? ¿No? Por qué no?
Probablemente tenga algo que ver con hacerlo más fácil de ver, el mismo problema aquí. Un GUID no le dice lo que está manejando mientras lo configura, lo depura o lo ejecuta.
Entonces, ¿qué se siente al apoyar este sistema? o para interactuar con él? Si no lo ha pensado, tómese un tiempo y considere el trabajo que está haciendo.
Tener información de ruta explícita sirve para ayudar a validar la solicitud. También ayuda a diferenciar la información lo suficiente como para que el soporte y los desarrolladores de sistemas posteriores puedan abordar estas URL y usarlas.
Es posible que desee imponer un límite de profundidad, simplemente para que un niño inteligente no obtenga simplemente la raíz de su sitio para cada operación que se les ocurra.
Si actualizar un recurso está afectando a su padre de una manera no trivial y predecible, tiene otros problemas ... Realmente necesita mirar el modelo de estado y descubrir por qué la información está saltando.
Si solo desea devolver una lista detallada de deltas, ¿por qué no? ¿Por qué no admitir varias vistas de salida conmutadas por varios parámetros? Jenkins devuelve sus respuestas API en una opción de xml o json y le permite especificar varios filtros para extraer el subárbol deseado.
Úselo usted mismo
Francamente, sin embargo, retrocede de lo que estás haciendo e intenta admitirlo, o crea otra aplicación para usarlo (no una de tus aplicaciones preexistentes). Haga algo similar para las API de terceros para tener un pequeño contexto de fondo.
Siempre que tenga que hacer algo que no resuelva directamente la solicitud de soporte, o que sea directamente necesario para la aplicación del cliente, entonces la API es menos que ideal, y sabe por qué es menos que ideal, lo que es aún mejor, porque puede arreglarlo o no cometer el mismo error.
Por ejemplo, si las solicitudes a una URL dada fallan constantemente, ¿cuánto esfuerzo tiene que invertir para determinar qué está fallando y por qué? ¿Qué pasos tomaste? ¿Podrías haber evitado uno de esos pasos al tener un mejor URI, o un mejor registro, o un mejor monitoreo, etc.?
Del mismo modo, si está escribiendo un nuevo cliente, ¿cuántas veces necesita consultar la documentación o el código fuente de la API? ¿Qué podrías hacer para reducir esa necesidad? ¿Qué podrías hacer para dejar de violar tus propias expectativas? ¿Qué podría hacer para simplificar el problema de las aplicaciones del cliente sin hacer que el servidor sea una pesadilla de mantener?
Manera correcta
Francamente, el camino correcto es circunstancial. REST es un conjunto de prácticas que funcionan en varias circunstancias para una serie de problemas. Si su problema no encaja, no lo haga, pero tampoco pretenda utilizar esas prácticas.
fuente
La mayoría de las características de las API REST están ahí por una razón, pero puede que no sea una razón relevante para usted. Dar todo el recurso, como en PUT, por ejemplo, es relevante si necesita idempotencia, de lo contrario no lo es. (Aunque creo que sería mejor para los usuarios / colegas / lo que sea anunciar el hecho de que su punto final no es idempotente al usar POST o PATCH en su lugar).
Por lo del camino, nunca he oído que esté relacionado con el descanso.
/root/345dd4dc-e175-455f-b545-85b1b1ce3e82
es tan parte de un árbol como/foo/bar/baz
. Tal vez un poco menos fácil de usar, pero no menos resty hasta donde puedo ver.Si desea un razonamiento más detallado sobre por qué REST fue diseñado tal como está, creo que debería leer la disertación original: https://www.ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation.pdf
Leyendo que podría descubrir que es bastante diferente de cómo REST se representa en las conversaciones o se usa en las API de hoy. Claramente, muchas otras personas han encontrado una razón, buena o mala, para apartarse de ella.
Particularmente me gusta esta cita que puede encontrar relevante:
fuente
PUT
solicitudes varias veces sin que usted lo sepa, y pueden hacerlo porque la especificación HTTP dice quePUT
es idempotente . Si susPUT
correos electrónicos no son idempotentes, esto interrumpirá su servicio .GET
yHEAD
son puros y sin efectos secundarios. Algunas personas perdieron datos, porque estaban usando aplicaciones web mal diseñadas, donde la eliminación del contenido se realizó con unaGET
solicitud, y el acelerador web felizmente envióGET
solicitudes a todos los URI que pudo encontrar.