Conceptos de API REST

10

Tengo tres preguntas sobre el diseño de REST API que espero que alguien pueda arrojar algo de luz. He buscado incansablemente durante muchas horas, pero no he encontrado respuestas a mis preguntas en ningún lado (¿tal vez no sé qué buscar?).

Pregunta 1

Mi primera pregunta tiene que ver con acciones / RPC. He estado desarrollando una API REST por un tiempo y estoy acostumbrado a pensar en términos de colecciones y recursos. Sin embargo, me he encontrado con un par de casos en los que el paradigma no parece aplicarse y me pregunto si hay una manera de conciliar esto con el paradigma REST.

Específicamente, tengo un caso en el que modificar un recurso hace que se genere un correo electrónico. Sin embargo, en un momento posterior, el usuario puede indicar específicamente que desea reenviar el correo electrónico que se envió anteriormente. Al reenviar el correo electrónico no se modifica ningún recurso. Ningún estado ha cambiado. Es simplemente una acción que debe ocurrir. La acción está vinculada al tipo de recurso específico.

¿Es apropiado mezclar algún tipo de llamada a la acción con un URI de recursos (por ejemplo /collection/123?action=resendEmail)? ¿Sería mejor especificar la acción y pasarle la identificación del recurso (por ejemplo /collection/resendEmail?id=123)? ¿Es esta la forma incorrecta de hacerlo? Tradicionalmente (al menos con HTTP) la acción que se realiza es el método de solicitud (GET, POST, PUT, DELETE), pero en realidad no permiten acciones personalizadas con un recurso.

Pregunta 2

Utilizo la parte de la cadena de consulta de la URL para filtrar el conjunto de recursos devueltos al consultar una colección (por ejemplo /collection?someField=someval). Luego, dentro de mi controlador API, determino qué tipo de comparación va a hacer con ese campo y valor. He descubierto que esto realmente no funciona. Necesito una forma de permitir que el usuario de la API especifique el tipo de comparación que desea realizar.

La mejor idea que se me ha ocurrido hasta ahora es permitir que el usuario de la API lo especifique como un apéndice del nombre del campo (por ejemplo /collection?someField:gte=someval, para indicar que debe devolver recursos donde someFieldsea ​​mayor o igual que (> =) sea lo que somevalsea ¿Es una buena idea? ¿Una mala idea? Si es así, ¿por qué? ¿Hay una mejor manera de permitir que el usuario especifique el tipo de comparación que se realizará con el campo y el valor dados?

Pregunta 3

A menudo veo URI que se parecen a algo así como /person/123/dogsobtener el persons dogs. En general, he evitado algo así porque al final me imagino que al crear un URI como ese, en realidad solo está accediendo a una dogscolección filtrada por una personID específica . Sería equivalente a /dogs?person=123. ¿Hay alguna vez una buena razón para que un URI REST tenga más de dos niveles de profundidad ( /collection/resource_id)?

Justin Warkentin
fuente
10
Tienes tres preguntas ¿Por qué no publicarlos por separado?
anaximander
3
Sería mejor dividir esto en 3 preguntas separadas. Un espectador puede obtener una excelente respuesta a una pero no a todas las preguntas.
2
Creo que todos están relacionados. El título es un poco de alto nivel, pero esta pregunta ayudará a muchas personas y se puede encontrar fácilmente durante una búsqueda SE. Esta pregunta debería convertirse en Community Wiki una vez que se hayan agregado suficientes votos y sustancia. Me llevó semanas investigar esto.
Andrew T Finnell
1
Puede haber sido mejor publicarlos por separado, IDK. Sin embargo, como mencionó @AndrewFinnell, pensé que sería una buena idea mantener las preguntas juntas ya que estas han sido las preguntas más difíciles relacionadas con REST que he tenido y sería bueno que otras personas puedan encontrar las respuestas juntos.
Justin Warkentin

Respuestas:

11

¿Es apropiado mezclar algún tipo de llamada a la acción con un URI de recursos (por ejemplo /collection/123?action=resendEmail)? ¿Sería mejor especificar la acción y pasarle la identificación del recurso (por ejemplo /collection/resendEmail?id=123)? ¿Es esta la forma incorrecta de hacerlo? Tradicionalmente (al menos con HTTP) la acción que se realiza es el método de solicitud (GET, POST, PUT, DELETE), pero en realidad no permiten acciones personalizadas con un recurso.

Prefiero modelar eso de una manera diferente, con una colección de recursos que represente los correos electrónicos que se enviarán; el envío será procesado por los internos del servicio a su debido tiempo, momento en el cual se eliminará el recurso correspondiente. (O el usuario podría ELIMINAR el recurso antes de tiempo, provocando la cancelación de la solicitud para realizar el envío).

Hagas lo que hagas, ¡no pongas verbos en el nombre del recurso! Ese es el sustantivo (y la parte de la consulta es el conjunto de adjetivos). Sustantivo verbos weirds REST!

Utilizo la parte de la cadena de consulta de la URL para filtrar el conjunto de recursos devueltos al consultar una colección (por ejemplo /collection?someField=someval). Luego, dentro de mi controlador API, determino qué tipo de comparación va a hacer con ese campo y valor. He descubierto que esto realmente no funciona. Necesito una forma de permitir que el usuario de la API especifique el tipo de comparación que desea realizar.

La mejor idea que se me ha ocurrido hasta ahora es permitir que el usuario de la API lo especifique como un apéndice del nombre del campo (por ejemplo /collection?someField:gte=someval, para indicar que debe devolver recursos donde someField es mayor o igual que ( >=) lo que somevalsea. ¿Es una buena idea? ¿Una mala idea? Si es así, ¿por qué? ¿Hay una mejor manera de permitir que el usuario especifique el tipo de comparación que se realizará con el campo y el valor dados?

Prefiero especificar una cláusula de filtro general y tener eso como un parámetro de consulta opcional en cualquier solicitud para recuperar el contenido de la colección. El cliente puede especificar exactamente cómo restringir el conjunto devuelto, de la forma que desee. También me preocuparía un poco la capacidad de detección del lenguaje de filtro / consulta; cuanto más rico lo hagas, más difícil será para los clientes arbitrarios descubrirlo. Un enfoque alternativo que, al menos en teoría, se ocupa de ese problema de detección es permitir que los recursos de restricción de la colección se obtengan, que los clientes obtienen al PUBLICAR un documento que describe la restricción al recurso de la colección. Sigue siendo un abuso leve, ¡pero al menos es uno que claramente puedes descubrir!

Este tipo de capacidad de descubrimiento es una de las cosas que encuentro menos fuertes con REST.

A menudo veo URI que se parecen /person/123/dogsa los perros. En general, he evitado algo así porque al final me imagino que al crear un URI como ese en realidad solo está accediendo a una colección de perros filtrada por una identificación de persona específica. Sería equivalente a /dogs?person=123. ¿Hay alguna vez una buena razón para que un URI REST tenga más de dos niveles de profundidad ( /collection/resource_id)?

Cuando la colección anidada es realmente una subcaracterística de las entidades miembro de la colección externa, es razonable estructurarlas como un recurso secundario. Por "subfunción" me refiero a algo como la relación de composición UML, donde destruir el recurso externo naturalmente significa destruir la colección interna.

Otros tipos de colección se pueden modelar como una redirección HTTP; por lo tanto, /person/123/dogsse puede responder haciendo un 307 que redirige a /dogs?person=123. En este caso, la colección no es en realidad composición UML, sino más bien agregación UML. La diferencia importa; es significativo!

Compañeros de Donal
fuente
2
Tienes puntos sólidos en general. Sin embargo, aunque la resendEmailacción podría manejarse creando una colección y PUBLICANDO en ella, eso parece menos natural. De hecho, no almaceno nada en la base de datos cuando se reenvía un correo electrónico (no es necesario). No se modifica ningún recurso, por lo tanto, es simplemente una acción que tiene éxito o falla. No pude devolver un ID de recurso que existe más allá de la vida de la llamada, lo que hace que dicha implementación sea un truco en lugar de ser RESTful. Simplemente no es una operación CRUD.
Justin Warkentin
3

Es comprensible estar un poco confundido acerca de cómo usar REST correctamente en función de todas las formas en que he visto a grandes empresas diseñar sus API REST.

Tiene razón en que REST es un sistema de recopilación de recursos. Es sinónimo de Transferencia de Estado de representación. No es una gran definición si me preguntas. Pero los conceptos principales son los 4 VERB HTTP y no tener estado.

La pieza importante a tener en cuenta es que solo tiene 4 VERBOS con REST. Estos son GET, POST, PUT y DELETE. Su resendejemplo sería agregar un nuevo verbo a REST. Esto debería ser una bandera roja.

Pregunta 1

Es importante darse cuenta de que la persona que llama desde su API REST no debería saber que realizar una búsqueda PUTen su colección podría generar un correo electrónico. Eso huele a una fuga para mí. Lo que podrían saber es que realizar un PUTresultado podría generar tareas adicionales que podrían consultar más tarde. Lo sabrían realizando un GETrecurso en el recurso creado recientemente. Eso GETdevolvería el recurso y todos los TaskID de recursos asociados con él. Luego puede consultar esas tareas para determinar su estado e incluso enviar una nueva Task.

Tienes pocas opciones.

REST: enfoque basado en recursos de tareas

Cree un tasksrecurso en el que pueda enviar tareas específicas a su sistema para realizar acciones. A continuación, puede realizar GETla tarea en función de la IDdevolución para determinar su estado.

O puede mezclar un SOAP over HTTPservicio web para agregar algo de RPC a su arquitectura.

consultar todas las tareas para un recurso específico

GET http://server/api/myCollection/123/tasks

{ "tasks" :
    [ { "22333" : "http://server/api/tasks/223333" } ] 
}

ejemplo de recurso de tarea

PUT http://server/api/tasks

{ 
    "type" : "send-email" , 
    "parameters" : 
    { 
         "collection-type" : "foo" , 
         "collection-id" : "123" 
    } 
}

==> devuelve el id de la tarea

223334

GET http://server/api/tasks/223334

{ 
    "status" : "complete" , 
    "date" : "whenever" 
}

REST- Uso de POST para desencadenar acciones

Siempre puede POSTdatos adicionales a un recurso. En mi opinión, esto violaría el espíritu de REST, pero seguiría cumpliendo.

Puedes hacer una POST similar a esta:

POST http://server/api/collection/123

{ "action" : "send-email" }

Actualizará el recurso 123 de la colección con datos adicionales. Esos datos adicionales son esencialmente una acción que le dice al servidor que envíe un correo electrónico para ese recurso.

El problema que tengo con esto es que un GETen el recurso devolverá estos datos actualizados. Sin embargo, esto resolvería sus requisitos y aún sería RESTful.

SOAP: servicio web que acepta recursos obtenidos de REST

Cree un nuevo servicio web en el que pueda enviar correos electrónicos basados ​​en el ID de recurso anterior de la API REST. No entraré en detalles sobre SOAP aquí, ya que la pregunta original es sobre REST y estos dos conceptos / tecnologías no deben compararse, ya que se trata de manzanas y naranjas .

Pregunta 2

También tiene algunas opciones aquí:

Parece que muchas compañías más grandes que publican API de REST exponen una searchcolección que realmente es solo una forma de pasar parámetros de consulta para devolver recursos.

GET http://server/api/search?q="type = myCollection & someField >= someval"

Lo que devolvería una colección de recursos REST completamente calificados como:

{
    "results" : 
       { [ 
             "location" : "http://server/api/myCollection/1",
             "location" : "http://server/api/myCollection/9",
             "location" : "http://server/api/myCollection/56"
         ]
       }
}

O puede permitir algo como MVEL como parámetro de consulta.

Pregunta 3

Prefiero los subniveles que tener que volver a subir y consultar el otro recurso con un parámetro de consulta. No creo que haya ninguna regla de una forma u otra. Puede implementar ambas formas y permitir que la persona que llama decida cuál es más apropiado en función de cómo ingresó por primera vez al sistema.

Notas

No estoy de acuerdo con los comentarios de legibilidad de otros. A pesar de lo que algunos podrían pensar, REST todavía no es para consumo humano. Es para el consumo de la máquina. Si quiero ver mis Tweets, uso el sitio web normal de Twitters. No realizo un REST GET con su API. Si quiero hacer algo programáticamente con mis tweets, entonces uso su API REST. Sí, las API deben ser comprensibles, pero tu gteno es tan malo, simplemente no es intuitivo.

La otra cosa principal con REST es que debería poder comenzar en cualquier punto dado en su API y navegar a todos los demás recursos asociados SIN conocer la URL exacta de los otros recursos con anticipación. Los resultados del GETVERBO en REST deberían devolver la URL REST completa de los recursos a los que hace referencia. Entonces, en lugar de una consulta que devuelve la ID de un Personobjeto, devolvería la URL totalmente calificada como http://server/api/people/13. Entonces siempre puede navegar programáticamente los resultados incluso si la URL ha cambiado.

Respuesta al comentario

En el mundo real, de hecho, hay cosas que deben suceder que no crean, leen, actualizan o eliminan (CRUD) un recurso.

Se pueden tomar acciones adicionales sobre los recursos. Las bases de datos relacionales típicas admiten el concepto de procedimientos almacenados. Estos son comandos adicionales que se pueden ejecutar en un conjunto de datos. REST no tiene ese concepto inherentemente. Y no hay razón para que así sea. Este tipo de acciones son perfectas para los servicios web RPC o SOAP.

Este es el problema general que veo con las API REST. A los desarrolladores no les gustan las limitaciones conceptuales que rodean REST, por lo que lo adaptan para hacer lo que quieran. Sin embargo, eso deja de ser un servicio RESTful. Esencialmente, esas URL se convierten en GETllamadas en servlets de tipo pseudo-REST.

Tienes pocas opciones:

  • Crear un recurso de tarea
  • Soporte POSTde datos adicionales al recurso para realizar una acción.
  • Agregue los comandos adicionales a través de un servicio web SOAP.

Si usara un parámetro de consulta, ¿qué VERBO HTTP usaría para reenviar el correo electrónico?

  • GET- ¿Esto reenvía el correo electrónico Y devuelve los datos del recurso? ¿Qué pasaría si un sistema almacenara en caché esa URL y la tratara como la URL única para ese recurso? Cada vez que llegan a la URL, reenvían un correo electrónico.
  • POST - En realidad no envió ningún dato nuevo al recurso, solo un parámetro de consulta adicional.

De acuerdo con todos los requisitos dados, resolver un problema POSTen el recurso con action fielddatos POST como.

Andrew T Finnell
fuente
3
Si bien REST implementado a través de HTTP le brinda esos 4 verbos, no estoy convencido de que esos verbos sean el final. En el mundo real, de hecho, hay cosas que deben suceder que no crean, leen, actualizan o eliminan (CRUD) un recurso. Reenviar un correo electrónico es una de esas cosas. No necesito almacenar ni modificar nada en la base de datos. Es simplemente una acción que tiene éxito o falla.
Justin Warkentin
@JustinWarkentin Entiendo cuáles son tus necesidades. Pero eso no hace que REST sea algo que no es. Agregar un nuevo verbo a la URL va en contra de la arquitectura REST. Actualizaré mi respuesta para ofrecer otra alternativa que sea RESTful.
Andrew T Finnell
@JustinWarkentin Mira 'REST - Usando POST para desencadenar acciones' en mi respuesta.
Andrew T Finnell
0

Pregunta 1: ¿Es apropiado mezclar algún tipo de llamada a la acción con un URI de recurso [o] sería mejor especificar la acción y pasarle la identificación del recurso?

Buena pregunta. En este caso, le aconsejaría que utilice el último enfoque, es decir, especifique la acción y le pase un ID de recurso. De esta manera, cuando su recurso se modifica por primera vez, a su vez llama a la /sendEmailacción (nota al margen: no es necesario llamarlo "reenviar") como una solicitud RESTful separada (que luego puede llamar una y otra vez, independientemente del recurso que se está modificando )

Pregunta 2: con respecto al uso de un operador de comparación así:/collection?someField:gte=someval

Si bien esto es técnicamente correcto, probablemente sea una mala idea. Uno de los principios clave de REST es la legibilidad. Le sugiero que simplemente pase el operador de comparación como otro parámetro, por ejemplo: /collection?someField=someval&operator=gtey, por supuesto, diseñe su API para que atienda un caso predeterminado (en el caso de que el operatorparámetro quede fuera del URI).

Pregunta 3: ¿Hay alguna vez una buena razón para que un URI REST tenga más de dos niveles de profundidad?

Sip; para la abstracción He visto un par de API REST que utilizan capas de abstracción a través de múltiples niveles de URI, por ejemplo: /vehicles/cars/123o /vehicles/bikes/123que a su vez le permite trabajar con información útil relacionada con ambos /vehiclesy las /vehicles/bikescolecciones. Dicho esto, no soy un gran admirador de este enfoque; rara vez necesitará hacer esto en la práctica, y es probable que pueda rediseñar la API para usar solo 2 niveles.

Y sí, como sugieren los comentarios anteriores, en el futuro sería mejor dividir sus preguntas en publicaciones separadas;)

Kosta Kontos
fuente
Creo que mi ejemplo para la pregunta # 2 fue demasiado simplista. Necesito especificar un operador de comparación para cada campo que se usa para filtrar la colección, no solo uno, por lo que en su ejemplo tendría que ser algo así /collection?field1=someval&field1Operator=gte&field2=someval&field2Operator=eq.
Justin Warkentin
0

Para la pregunta 2, una alternativa diferente puede ser más flexible: considere cada búsqueda como un recurso que el usuario construye antes de usar.

supongamos que tiene un contenedor de "búsquedas", allí hace una POST /api/searches/con la especificación de consulta en el contenido. podría ser un documento JSON, XML o incluso un documento SQL, lo que sea más fácil para usted. Si la consulta se analiza correctamente, se crea una nueva búsqueda como un nuevo recurso con su propio URI, digamos/api/searches/q123/

Entonces, el cliente puede simplemente GET /api/searches/q123/recuperar los resultados de la consulta.

Finalmente, puede pedirle al cliente que elimine la consulta o purgarla después de cerrar la sesión.

Javier
fuente
0

¿Es apropiado mezclar algún tipo de llamada a la acción con un URI de recursos (por ejemplo, / colección / 123? Action = resendEmail)? ¿Sería mejor especificar la acción y pasarle la identificación del recurso (p. Ej. / Collection / resendEmail? Id = 123)? ¿Es esta la forma incorrecta de hacerlo? Tradicionalmente (al menos con HTTP) la acción que se realiza es el método de solicitud (GET, POST, PUT, DELETE), pero en realidad no permiten acciones personalizadas con un recurso.

No, no es apropiado, ya que los IRI son para identificar recursos y no operaciones (sin embargo, ppl utiliza este método de anulación de enfoque por un tiempo, en casos en los que no se admiten métodos que no son POST y GET). Lo que puede hacer es buscar un método HTTP apropiado o crear uno nuevo. POST puede ser tu amigo en estos casos (por favor, úsalo si no pueden encontrar un método apropiado y la solicitud no se recupera). Otro enfoque para POST /emailsgenerar recursos a partir del envío de correo electrónico y, por lo tanto, puede enviar los correos electrónicos sin crear un recurso real. Por cierto. La estructura de URI no lleva semántica, por lo que desde una perspectiva REST realmente no importa qué tipo de URI use. Lo que importa son los metadatos (por ejemplo, relación de enlace ) asignados a los enlaces que envió a los clientes.

La mejor idea que se me ha ocurrido hasta ahora es permitir que el usuario de la API lo especifique como un apéndice del nombre del campo (por ejemplo, / colección? SomeField: gte = someval - para indicar que debe devolver recursos donde someField es mayor que o igual a (> =) lo que sea algún valor. ¿Es una buena idea? ¿Una mala idea? Si es así, ¿por qué? ¿Hay una mejor manera de permitir que el usuario especifique el tipo de comparación para realizar con el campo y el valor dados?

No tiene que crear un lenguaje de consulta propio. Prefiero usar uno ya existente y agregar alguna descripción de consulta a los metadatos del enlace. Probablemente debería usar un tipo de medio RDF (por ejemplo, JSON-LD) para hacer eso o usar un tipo MIME personalizado (afaik no hay formato que no sea RDF que lo admita). El uso de estándares existentes desacopla a su cliente del servidor, de eso se trata la restricción de interfaz uniforme.

Sería equivalente a / dogs? Person = 123. ¿Hay alguna vez una buena razón para que un URI REST tenga más de dos niveles de profundidad (/ collection / resource_id)?

Como mencioné anteriormente, la estructura URI no importa desde una perspectiva REST. Podrías usar /x71fd823df2por ejemplo. Todavía tendría sentido para los clientes porque verifican los metadatos asignados a los enlaces y no la estructura de URI. El objetivo principal de URI es identificar recursos. En el estándar URI, afirman que la ruta contiene datos jerárquicos y que la consulta contiene datos no jerárquicos. Pero puede ser muy subjetivo lo que es jerárquico. Es por eso que cumple con múltiples niveles de URI profundos y URI con consultas largas.

He buscado incansablemente durante muchas horas, pero no he encontrado respuestas a mis preguntas en ningún lado (¿tal vez no sé qué buscar?).

Debe leer al menos las restricciones REST de la disertación Fielding , el estándar HTTP y probablemente las API web de tercera generación de Markus.

inf3rno
fuente