¿Por qué apuntar a un diseño RESTful?
Los principios RESTful traen las características que hacen que los sitios web sean fáciles (para que un usuario humano aleatorio los "navegue") al diseño de la API de servicios web , por lo que son fáciles de usar para un programador. REST no es bueno porque es REST, es bueno porque es bueno. Y es bueno sobre todo porque es simple .
La simplicidad de HTTP simple (sin sobres SOAP y POST
servicios sobrecargados de URI único ), lo que algunos pueden llamar "falta de características" , es en realidad su mayor fortaleza . De inmediato, HTTP le pide que tenga direccionabilidad y apatridia : las dos decisiones de diseño básicas que mantienen a HTTP escalable hasta los mega-sitios (y mega-servicios) actuales.
Pero REST no es el bulltet plateado: a veces, un estilo RPC ("Llamada a procedimiento remoto", como SOAP) puede ser apropiado , y otras necesidades tienen prioridad sobre las virtudes de la Web. Esto esta bien. Lo que realmente no nos gusta es la complejidad innecesaria . Con demasiada frecuencia, un programador o una empresa incorporan servicios de estilo RPC para un trabajo que HTTP antiguo podría manejar perfectamente. El efecto es que HTTP se reduce a un protocolo de transporte para una enorme carga útil XML que explica lo que "realmente" está sucediendo (no el URI o el método HTTP dan una pista al respecto). El servicio resultante es demasiado complejo, imposible de depurar y no funcionará a menos que sus clientes tengan la configuración exacta que el desarrollador pretendía.
De la misma manera, un código Java / C # no puede estar orientado a objetos, solo usar HTTP no hace que un diseño RESTful. Uno puede estar atrapado en la prisa de pensar en sus servicios en términos de acciones y métodos remotos que deberían llamarse. No es de extrañar que esto termine principalmente en un servicio de estilo RPC (o un híbrido REST-RPC). El primer paso es pensar de manera diferente. Se puede lograr un diseño RESTful de muchas maneras, una es pensar en su aplicación en términos de recursos, no de acciones:
💡 En lugar de pensar en términos de acciones que puede realizar ("hacer una búsqueda de lugares en el mapa") ...
... trate de pensar en términos de los resultados de esas acciones ("la lista de lugares en el mapa que coincide con un criterio de búsqueda").
Iré por los ejemplos a continuación. (Otro aspecto clave de REST es el uso de HATEOAS: no lo cepillo aquí, pero lo hablo rápidamente en otra publicación ).
Problemas del primer diseño
Echemos un vistazo al diseño propuesto:
ACTION http://api.animals.com/v1/dogs/1/
En primer lugar, no deberíamos considerar crear un nuevo verbo HTTP ( ACTION
). En términos generales, esto no es deseable por varias razones:
- (1) Dado solo el URI del servicio, ¿cómo sabrá un programador "aleatorio"
ACTION
existe verbo?
- (2) si el programador sabe que existe, ¿cómo sabrá su semántica? ¿Qué significa ese verbo?
- (3) ¿qué propiedades (seguridad, idempotencia) debería uno esperar que tenga ese verbo?
- (4) ¿qué pasa si el programador tiene un cliente muy simple que solo maneja verbos HTTP estándar?
- (5) ...
Ahora consideremos usarPOST
(discutiremos por qué a continuación, solo tome mi palabra ahora):
POST /v1/dogs/1/ HTTP/1.1
Host: api.animals.com
{"action":"bark"}
Esto podría estar bien ... pero solo si :
{"action":"bark"}
era un documento; y
/v1/dogs/1/
era un URI de "procesador de documentos" (similar a una fábrica). Un "procesador de documentos" es un URI en el que simplemente "arrojaría cosas" y "olvidaría" sobre ellos; el procesador puede redirigirlo a un recurso recién creado después del "lanzamiento". Por ejemplo, el URI para publicar mensajes en un servicio de intermediario de mensajes, que, después de la publicación, lo redirigiría a un URI que muestra el estado del procesamiento del mensaje.
No sé mucho sobre su sistema, pero apuesto a que ambos no son ciertos:
{"action":"bark"}
no es un documento , en realidad es el método que está tratando de introducir ninja en el servicio; y
- el
/v1/dogs/1/
URI representa un recurso "perro" (probablemente el perro con id==1
) y no un procesador de documentos.
Entonces, todo lo que sabemos ahora es que el diseño anterior no es tan RESTANTE, pero ¿qué es eso exactamente? ¿Qué tiene de malo? Básicamente, es malo porque es un URI complejo con significados complejos. No se puede inferir nada de eso. ¿Cómo sabría un programador que un perro tiene una bark
acción que se puede infundir secretamente con un perro POST
?
Diseñando las llamadas a la API de tu pregunta
Así que vamos al grano e intentemos diseñar esos ladridos RESTAMENTE pensando en términos de recursos . Permítame citar el libro de Restful Web Services :
Una POST
solicitud es un intento de crear un nuevo recurso a partir de uno existente. El recurso existente puede ser el padre del nuevo en un sentido de estructura de datos, la forma en que la raíz de un árbol es el padre de todos sus nodos hoja. O el recurso existente puede ser un recurso especial de "fábrica"
cuyo único propósito es generar otros recursos. La representación enviada junto con una POST
solicitud describe el estado inicial del nuevo recurso. Al igual que con PUT, una POST
solicitud no necesita incluir una representación en absoluto.
Siguiendo la descripción anterior, podemos ver que bark
puede modelarse como un subrecurso de adog
(dado que a bark
está contenido dentro de un perro, es decir, una corteza es "ladrada" por un perro).
De ese razonamiento ya obtuvimos:
- El método es
POST
- El recurso es
/barks
, sub-recurso de dog:, que /v1/dogs/1/barks
representa una bark
"fábrica". Ese URI es único para cada perro (ya que está debajo /v1/dogs/{id}
).
Ahora cada caso de su lista tiene un comportamiento específico.
1. ladrar solo envía un correo electrónico a dog.email
y no registra nada.
En primer lugar, ¿ladrar (enviar un correo electrónico) es una tarea síncrona o asíncrona? En segundo lugar, ¿la bark
solicitud requiere algún documento (el correo electrónico, tal vez) o está vacío?
1.1 ladrar envía un correo electrónico a dog.email
y no registra nada (como una tarea sincrónica)
Este caso es simple. Una llamada al barks
recurso de la fábrica produce un ladrido (un correo electrónico enviado) de inmediato y la respuesta (si está bien o no) se da de inmediato:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(entity-body is empty - or, if you require a **document**, place it here)
200 OK
Como no registra (cambia) nada, 200 OK
es suficiente. Muestra que todo salió como se esperaba.
1.2 ladrar envía un correo electrónico dog.email
y no registra nada (como una tarea asincrónica)
En este caso, el cliente debe tener una forma de rastrear la bark
tarea. La bark
tarea debería ser un recurso con su propio URI .:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed;
NOTE: when possible, the response SHOULD contain a short hypertext note with a hyperlink
to the newly created resource (bark) URI, the same returned in the Location header
(also notice that, for the 202 status code, the Location header meaning is not
standardized, thus the importance of a hipertext/hyperlink response)}
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
De esta manera, cada uno bark
es rastreable. El cliente puede emitir un GET
a la bark
URI para saber su estado actual. Tal vez incluso use a DELETE
para cancelarlo.
2. ladrar envía un correo electrónico dog.email
y luego incrementadog.barkCount
en 1
Este puede ser más complicado si desea que el cliente sepa que el dog
recurso ha cambiado:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
{document body, if needed; when possible, containing a hipertext/hyperlink with the address
in the Location header -- says the standard}
303 See Other
Location: http://api.animals.com/v1/dogs/1
En este caso, la location
intención del encabezado es hacerle saber al cliente que debe echarle un vistazo dog
. Del HTTP RFC sobre303
:
Este método existe principalmente para permitir que la salida de una
POST
secuencia de comandos activada redirija al agente de usuario a un recurso seleccionado.
Si la tarea es asíncrona, bark
se necesita un subrecurso al igual que la 1.2
situación y 303
debe devolverse en el GET .../barks/Y
momento en que se completa la tarea.
3. ladrar crea un nuevo " bark
" registro con bark.timestamp
grabación cuando se produce la corteza. También se incrementa dog.barkCount
en 1.
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
201 Created
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
Aquí, bark
se crea debido a la solicitud, por lo que 201 Created
se aplica el estado .
Si la creación es asíncrona, 202 Accepted
se requiere a (en su lugar, como dice el RFC de HTTP ).
La marca de tiempo guardada es parte del bark
recurso y se puede recuperar con un GET
. El perro actualizado puede ser "documentado" en eso GET dogs/X/barks/Y
también.
4. ladrar ejecuta un comando del sistema para extraer la última versión del código de perro de Github. Luego envía un mensaje de texto para dog.owner
decirles que el nuevo código de perro está en producción.
La redacción de este es complicada, pero es una tarea asincrónica simple:
POST /v1/dogs/1/barks HTTP/1.1
Host: api.animals.com
Authorization: Basic mAUhhuE08u724bh249a2xaP=
(document body, if needed)
202 Accepted
Location: http://api.animals.com/v1/dogs/1/barks/a65h44
Luego, el cliente emitiría GET
s para /v1/dogs/1/barks/a65h44
conocer el estado actual (si se extraía el código, se enviaba el correo electrónico al propietario y tal). Cada vez que el perro cambia, a 303
es aplicable.
Terminando
Citando a Roy Fielding :
Lo único que REST requiere de los métodos es que se definan de manera uniforme para todos los recursos (es decir, para que los intermediarios no tengan que conocer el tipo de recurso para comprender el significado de la solicitud).
En los ejemplos anteriores, POST
está diseñado uniformemente. Hará al perro " bark
". Eso no es seguro (lo que significa que la corteza tiene efectos sobre los recursos), ni idempotente (cada solicitud produce un nuevo bark
), que se ajusta bien al POST
verbo.
Un programador sabría: a POST
para producir barks
unbark
. Los códigos de estado de respuesta (también con entidad-cuerpo y encabezados cuando es necesario) hacen el trabajo de explicar qué cambió y cómo el cliente puede y debe proceder.
Nota: Las fuentes principales utilizadas fueron: el libro " Restful Web Services ", el HTTP RFC y el blog de Roy Fielding .
Editar:
La pregunta y, por lo tanto, la respuesta han cambiado bastante desde su creación. La pregunta original fue sobre el diseño de un URI como:
ACTION http://api.animals.com/v1/dogs/1/?action=bark
A continuación se muestra la explicación de por qué no es una buena opción:
Cómo los clientes le dicen al servidor QUÉ HACER con los datos es la información del método .
- Los servicios web RESTful transmiten información del método en el método HTTP.
- Los servicios típicos de estilo RPC y SOAP mantienen los suyos en el cuerpo de la entidad y el encabezado HTTP.
EN QUÉ PARTE de los datos [el cliente quiere que el servidor] opere es la información de alcance .
- Los servicios RESTful usan el URI. Los servicios SOAP / RPC-Style utilizan una vez más el cuerpo de la entidad y los encabezados HTTP.
Como ejemplo, tome el URI de Google http://www.google.com/search?q=DOG
. Allí, la información del método es GET
y la información del alcance es /search?q=DOG
.
Larga historia corta:
- En arquitecturas RESTful , la información del método entra en el método HTTP.
- En las arquitecturas orientadas a recursos , la información de alcance va al URI.
Y la regla de oro:
Si el método HTTP no coincide con la información del método, el servicio no es RESTful. Si la información de alcance no está en el URI, el servicio no está orientado a los recursos.
Puede poner la " acción" "ladrar" en la URL (o en el cuerpo de la entidad) y usar POST
. No hay problema, funciona, y puede ser la forma más sencilla de hacerlo, pero esto no es RESTful .
Para mantener su servicio realmente RESTANTE, es posible que tenga que dar un paso atrás y pensar qué es lo que realmente quiere hacer aquí (qué efectos tendrá en los recursos).
No puedo hablar sobre sus necesidades comerciales específicas, pero permítame darle un ejemplo: considere un servicio de pedidos RESTful donde los pedidos están en URI como example.com/order/123
.
Ahora digamos que queremos cancelar un pedido, ¿cómo lo haremos? Uno puede estar tentado a pensar que es una "acción" de "cancelación " y diseñarlo comoPOST example.com/order/123?do=cancel
.
Eso no es RESTful, como hemos dicho anteriormente. En cambio, podríamos PUT
una nueva representación de la order
con un canceled
elemento enviado a true
:
PUT /order/123 HTTP/1.1
Content-Type: application/xml
<order id="123">
<customer id="89987">...</customer>
<canceled>true</canceled>
...
</order>
Y eso es. Si el pedido no se puede cancelar, se puede devolver un código de estado específico. (Un diseño de subrecursos, como POST /order/123/canceled
con el cuerpo de la entidad true
puede, por simplicidad, también estar disponible).
En su escenario específico, puede intentar algo similar. De esa manera, mientras un perro ladra, por ejemplo, un GET
at /v1/dogs/1/
podría incluir esa información (por ejemplo <barking>true</barking>
) . O ... si eso es demasiado complicado, afloje su requisito RESTful y siga conPOST
.
Actualizar:
No quiero que la respuesta sea demasiado grande, pero me toma un tiempo acostumbrarme a exponer un algoritmo (una acción ) como un conjunto de recursos. En lugar de pensar en términos de acciones ( "hacer una búsqueda de lugares en el mapa" ), uno debe pensar en términos de los resultados de esa acción ( "la lista de lugares en el mapa que coincide con un criterio de búsqueda" ).
Es posible que vuelva a este paso si descubre que su diseño no se ajusta a la interfaz uniforme de HTTP.
Las variables de consulta son información de alcance , pero no denotan nuevos recursos ( /post?lang=en
es claramente el mismo recurso que /post?lang=jp
, solo una representación diferente). Más bien, se utilizan para transmitir el estado del cliente (como ?page=10
, por ejemplo , ese estado no se mantiene en el servidor; ?lang=en
también es un ejemplo aquí) o parámetros de entrada a recursos algorítmicos ( /search?q=dogs
, /dogs?code=1
). De nuevo, no recursos distintos.
Propiedades de los verbos HTTP (métodos):
Otro punto claro que se muestra ?action=something
en el URI no es RESTful, son las propiedades de los verbos HTTP:
GET
y HEAD
son seguros (e idempotentes);
PUT
y DELETE
solo son idempotentes;
POST
Es ninguno.
Seguridad : Una GET
o HEAD
solicitud es una solicitud para leer algunos datos, no una solicitud para cambiar cualquier estado del servidor. El cliente puede hacer GET
o HEAD
solicitar 10 veces y es lo mismo que hacerlo una vez, o nunca hacerlo .
Idempotencia : una operación idempotente en una que tiene el mismo efecto si la aplicas una o más de una vez (en matemáticas, multiplicar por cero es idempotente). Si tiene DELETE
un recurso una vez, la eliminación nuevamente tendrá el mismo efecto (el recurso GONE
ya está ).
POST
No es seguro ni idempotente. Hacer dos POST
solicitudes idénticas a un recurso 'de fábrica' probablemente dará como resultado dos recursos subordinados que contengan la misma información. Con sobrecargado (método en URI o entidad-cuerpo) POST
, todas las apuestas están desactivadas.
Ambas propiedades fueron importantes para el éxito del protocolo HTTP (a través de redes poco confiables): ¿cuántas veces ha actualizado (GET
) la página sin esperar hasta que esté completamente cargada?
Crear una acción y colocarla en la URL claramente rompe el contrato de los métodos HTTP. Una vez más, la tecnología te permite, puedes hacerlo, pero ese no es un diseño RESTful.