29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 1/13
Servicios web (3): Diseñando servicios
REST (1 )
03/09/2010 por Enrique Amodeo [1]
Bueno, bueno, recién vuelvo de vacaciones, ¡ y me doy cuenta que no he
publicado nada en todo Agosto ! Pues nada, a escribir un poco, que se que a
algunos de vosotros os viene bien leer mis ladrillos para combatir el insomnio.
Comienza aquí una serie de posts sobre diseño de servicios REST, y como estoy
bajo de forma debido a los excesos veraniegos, este primer post sólo pretende
abordar lo más básico. En siguientes entregas iré atacando conceptos más
complejos. No es mi intención enseñar código, ya que eso depende de la
plataforma de desarrollo que uséis, y al fin y al cabo los servicios web se basan
en definir una buena interfaz, y no en como estén implementados
internamente. Pretendo describir una serie de técnicas de diseño y modelado
que os sirvan para definir servicios REST de una manera adecuada a vuestras
necesidades. Son técnicas que he ido aprendiendo por mi cuenta mediante la
práctica en la vida real, ya que he encontrado bastante poco en la web sobre
diseño REST.
Como ya vimos en mi anterior post [2], el protocolo idóneo para hacer servicios
web basados en principios REST es el protocolo HTTP. Veamos como podemos
combinar las URIs, con los verbos HTTP y los formatos multimedia para publicar
la funcionalidad básicas de un CRUD normalito en forma de servicios REST. Al
fin y al cabo nuestro jefe lo primero que va a querer es un “mantenimiento de
tablas” para mañana. Sin embargo tened en mente que el REST es más que un
CRUD, como iremos viendo a lo largo de la serie. Para enfrentarnos a un CRUD
básico tenemos varios patrones de diseño: entidad/colección, singleton y
maestro/esclavo.
El más básico es el patrón entidad/colección. Se basa en conceptos clásicos de
OO y bases de datos relacionales y los adapta al mundo web. Básicamente
consiste en definir entidades de negocio, agrupadas por tipos o clases en
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 2/13
colecciones. Más formalmente, una entidad es un recurso REST que representa
un objeto del dominio de negocio que es persistente, y tiene una identidad
propia. Cada entidad puede tener, opcionalmente, un estado, que contiene
información adicional o “datos” sobre la entidad. Estos datos pueden ser leídos
y modificados. Las entidades pueden ser borradas y creadas. Ejemplos de
entidades: cliente, libro (en una tienda de libros), post (en un sistema de blogs),
cuenta corriente, etc.
El hecho de que las entidades necesiten una identidad encaja naturalmente con
REST, ya que todos los recursos necesitan un identificador único global, que
podemos usar como identidad de la entidad. Obviamente vamos a modelar esa
identificación como una URI. Podemos usar cualquier URI que queramos a
condición de que sea única por cada instancia de entidad. En el patrón
entidad/colección propongo modelar las URIs siguiendo las siguiente
nomenclatura:
URI Colección: http(s)://www.myempresa.com/<sistema>/<coleccion> Ej.
https://www.bookstore.com/shop/books [3]
URI Entidad: http(s)://www.myempresa.com/<sistema>/<coleccion>/<id
instancia entidad> Ej.
https://www.bookstore.com/shop/books/XSR9RFSV43r52 [4]
Donde <sistema> es el nombre de nuestro sistema de información o el nombre
de nuestro proyecto, <coleccion> el nombre de la colección a la que pertenece la
entidad y por último tenemos el id de la instancia concreta a la que queremos
acceder. Este tipo de URIs se llama URIs constructivas. Existe una escuela REST
que defiende las URIs opacas sobre las constructivas, en otro post veremos la
diferencia y las ventajas e inconvenientes entre ambas. Es importante que el id
de instancia no sea predecible, como sería el caso de un numérico
autoincremental. Es mejor usar un UUID tipo 4 o un número aleatorio con hash
criptográfica. De esta manera nos evitaremos ataques de búsqueda de id de
instancias.
Si la entidad, como suele ser el caso, tiene estado, es decir, información
públicamente manipulable, el siguiente paso que debemos dar es decidir que
tipos mime vamos a soportar. Dichos tipos mime deben poder transportar la
información pertinente sobre la entidad. Tal vez podríamos usar
application/xml o application/json, para transportar información estructurada.
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 3/13
Pero podemos usar también video, imágenes, sonidos, pdf o gráficos vectoriales
para transportar información “no estructurada”. Como se aprecia un sólo tipo
mime puede ser insuficiente para cubrir todos los aspectos funcionales de la
entidad. Un detalle importante es que cada operación sobre la misma entidad
puede soportar diferentes tipos mime. Incluso en teoría, cada instancia podría
soportar un juego diferente de tipos mime en función de su estado, aunque yo
nunca me he encontrado con este último caso.
Para poder leer (R) las entidades debemos decidir que verbo HTTP usar. En este
caso el verbo adecuado es GET. El verbo GET está definido explícitamente en la
especificación HTTP con el significado de leer, así que lo usaremos para esta
operación. Además GET esta clasificado como seguro e idempotente. Seguro en
este contexto significa que el servidor no va a cambiar su estado, ni a generar
ningún efecto secundario (como el envío de un mail) detectable por el cliente,
una propiedad interesante ya que al leer no queremos que ocurran cosas “raras”.
Lo de idempotente significa que podemos repetir cuantas veces queramos la
operación, que dado los mismos parámetros, el resultado es exactamente el
mismo, asumiendo que el estado del servidor no ha sido modificado mientras
tanto, claro. Una lectura típica consistiría en enviar una petición GET a la URI
de la entidad. Con la petición debemos enviar la cabecera Accept especificando
el tipo mime que soportamos como cliente. El servidor, en caso de éxito,
responde con un mensaje con estatus 200, cabecera Content-Type especificando
el tipo mime de la respuesta y un cuerpo con los datos. Otra opción es que el
servidor responda con 204, indicando que no hay datos que devolver. Los
mensajes HTTP en estos casos quedarían:
Petición de lectura de una entidad:
GET https://www.mybooks.com/books/XSR9RFSV43r52 HTTP/1.1
Accept: application/json,application/xml;q=0.5
Respuesta exitosa, pero sin datos:
HTTP/1.1 204 No Content
Respuesta exitosa, con datos:
HTTP/1.1 200 Ok
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 4/13
Content-Type: application/json
{
'id':'XSR9RFSV43r52',
'title':'Como hacerse rico',
'precio':'20euro'
}
¿Cómo hacemos las consultas? Sencillo y complejo a la vez. Obviamente
podemos hacer una petición GET contra la URI de la colección. En este caso,
¿qué devolvería el servidor? Tenemos varias opciones:
Si hay datos, un código 200 y en el cuerpo de la respuesta un array o lista,
que contiene enlaces a las entidades pertenecientes a la colección. Esta lista
se puede codificar fácilmente en JSON, XML o casi cualquier tipo mime
estructurado. Con enlaces me refiero a las URIs de las entidades. Si usamos la
nomenclatura de las URIs propuestas anteriormente, basta con mandar el id
de entidad, porque concatenándolo con la URI de la colección tenemos la
URI de la entidad. El cliente puede después usar estas URIs para acceder a los
datos de la entidad. Por ejemplo, los cuerpos de las respuestas HTTP
quedarían:
Usando application/json:
["XSR9RFSV43r52", "X35RFSV4AT52"]
Usando application/xml:
<books>
<book ref="XSR9RFSV43r52"/>
<book ref="X35RFSV4AT52"/>
</books>
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 5/13
Con el enfoque anterior tenemos un uso de ancho de banda muy pequeño, es
ideal si sólo queremos acceder y operar con los datos de una pocas de las
entidades devueltas por la consulta. Si queremos en cambio acceder a casi
todas las entidades devueltas, tendremos un problema de rendimiento, al
tener que hacer muchos accesos al servidor. Para minimizar este problema
podemos traer, no sólo los identificadores de instancia, sino todos los datos
de la entidad, de esta manera nos evitamos tener que acceder de nuevo al
servidor. Usando application/json, por ejemplo, el cuerpo de la respuesta
HTTP contendría:
[
{
'id':'XSR9RFSV43r52',
'title':'Como hacerse rico',
'precio':'20euro'
},
{
'id':'X35RFSV4AT52',
'title':'Los robots oxidados',
'precio':'18.4euro'
}
]
En el caso de que la consulta devuelva muchas entidades los métodos
anteriores pueden ser ineficientes. Una opción interesante es estructurar la
respuesta en páginas de resultados. De esta forma la consulta no devolvería
un array, sino la primera página de resultados. Cada página contendría un
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 6/13
array con un número máximo de resultados, y al menos un enlace a la
siguiente página. Podemos añadir enlaces adicionales: página anterior,
primera página, última página, las 10 siguientes páginas. También podemos
añadir datos extra como el número total de entidades devueltas en la
consulta. El array de resultados, puede contener como antes, o sólo los
enlaces a las entidades, o también los datos de éstas. Si usamos URIs
relativas, y application/json, el cuerpo de la respuesta HTTP quedaría:
{
'totalBooksFound':54345,
'pageSize':2,
'firstPage':'/shop/books',
'prevPage':'/shop/books',
'nextPage':'/shop/books?page=2',
'lastPage':'/shop/books?page=27173',
'books':[
{
'id':'XSR9RFSV43r52',
'title':'Como hacerse rico',
'precio':'20euro'
},
{
'id':'X35RFSV4AT52',
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 7/13
'title':'Los robots oxidados',
'precio':'18.4euro'
}
]
}
El usar una opción u otra es cuestión del problema que tengamos a mano, y
debéis decidir vosotros. Lo más simple es mandar un array, ya sea en JSON o
XML con enlaces a todas las entidades de la aplicación. Lo más complejo, pero
más potente, es devolver páginas de resultados. En el caso de que la consulta no
devuelva datos podemos devolver o bien un 204 sin cuerpo de mensaje o bien
un 200 con un array vacío o una página vacía.
Esto nos resuelve el problema de consultar todas las entidades de una colección.
Si queremos restringir la consulta mediante criterios de búsqueda, debemos
mandar parámetros al servidor conteniendo dichos criterios, ¿como los
mandamos? ¿En cabeceras HTTP? ¿En el cuerpo de la petición? Mi consejo es
que uséis una “query string”. Dentro de la especificación de las URIs, se define
que la parte “query string” de la URI, el medio adecuado para restringir el
resultado de una operación HTTP sobre una URI. Así quedaría:
http(s)://www.myempresa.com/<sistema>/<coleccion>?<param1>=<value1>&
<param2>=<value2> Por ejemplo: https://www.bookstore.com/shop/books?
page=4&tituloContiene=robots&precioMaximo=20euro [5]
Para borrar (D) una entidad, el método a usar es DELETE sobre la URI de la
entidad. Obviamente este método no es seguro, ya que estamos eliminando
información del servidor, pero si es idempotente. Esto nos permite hacer
DELETE varias veces sobre la entidad, en caso de que no tengamos claro si la
petición anterior se llegó al servidor o no. Las peticiones duplicadas serán
ignoradas y no se producirán efectos indeseados en el servidor. La respuesta de
un DELETE correctamente ejecutado no transporta datos, y por lo tanto tiene el
estatus 204.
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 8/13
Para hacer un borrado masivo, podemos hacer un DELETE sobre la URI de la
colección, en cuyo caso se borrarían todas las entidades pertenecientes a la
colección. Desde un punto de vista purista, un DELETE sobre la URI de la
colección, sin “query string”, debería borrar el recurso colección, con lo que un
siguiente acceso a dicho recurso daría un error 404 Not Found, al dejar de existir
la colección. Podemos ignorar esta interpretación del significado de DELETE, y
sólo borrar las entidades, pero perderíamos interoperabilidad. Otra opción,
mucho más interesante, es hacer DELETE sobre la URI de la colección pero con
parámetros en su “query string”, en cuyo caso sólo se borrarían las entidades
que coinciden con el criterio definido en dichos parámetros. Sería un borrado en
masa, pero limitado por un critério de búsqueda.
Ahora que podemos leer entidades, consultar colecciones y borrar recursos,
¿cómo podemos modificar el estado de un recurso REST? En HTTP tenemos dos
métodos inseguros, es decir, que modifican el estado del servidor. Estos
métodos son PUT y POST, ambos permiten “escribir” en el servidor pero son
sutilmente distintos. PUT es idempotente, mientras que POST no lo es. De esta
forma repetir varias veces un PUT sobre la misma URI con los mismos
parámetros y cuerpo de mensaje, tiene el mismo efecto que hacerlo una única
vez. Por el contrario repetir un POST contra la misma URI no es lo mismo que
hacerlo una única vez.
¿Cómo podemos hacer pues una operación de actualización (U) de los datos de
una entidad? Depende. Si queremos sobrescribir el estado de la entidad, o una
parte de este, debemos hacer un PUT. Esto es debido a que un UPDATE clásico
es idempotente, por lo tanto el método adecuado es PUT. Basta con hacer un
PUT a la URI de la entidad, con la cabecera Content-Type indicando el tipo
mime del mensaje, y un cuerpo con el nuevo estado de la entidad. Si tiene éxito
el servidor devuelve un código 204. Otra opción es que el servidor devuelva un
código 200 adjuntando el nuevo estado de la entidad tras la actualización. Esta
última opción es buena si el servidor realiza alguna transformación adicional
sobre la entidad como consecuencia de la actualización. Una opción de diseño a
tener en cuenta es permitir al cliente no incluir toda la información en el
mensaje del PUT, sino sólo la información que desea a modificar. Por ejemplo,
sólo tendríamos que enviar los campos nombre y edad, si no queremos
modificar todos los datos de la persona. Los mensajes HTTP quedarían:
Petición de actualización de una entidad:
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 9/13
PUT https://www.mybooks.com/books/XSR9RFSV43r52 HTTP/1.1
Content-Type: application/json
Accept: application/json,application/xml;q=0.5
{
'title':'Como hacerse rico rápido',
'precio':'20euro'
}
Respuesta exitosa, pero sin datos:
HTTP/1.1 204 No Content
Respuesta exitosa, con datos:
HTTP/1.1 200 Ok
Content-Type: application/json
{
'id':'XSR9RFSV43r52',
'title':'Como hacerse rico rápido',
'precio':'20euro',
'specialOffer':true <-- Flag specialOffer puesto por el servidor
}
¿Y una actualización masiva de varias entidades? Hacer un PUT a la URI de la
colección con la “query string” adecuada. El servidor sólo debería actualizar las
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 10/13
entidades que cumplan los criterios especificados. Puede devolver un 204 o un
200 con información adicional, como cuantas entidades se han modificado.
Finalmente sólo nos queda crear (C) entidades. El protocolo HTTP nos permite
crear recursos tanto con PUT como con POST. Yo desaconsejo usar PUT, ya que
implica que el cliente decida que URI va a tener el nuevo recurso, lo que añade
una nueva responsabilidad sobre el cliente: saber montar la nueva URI de forma
correcta, según la nomenclatura definida por el servidor. Esto, además de
aumentar el acoplamiento cliente/servidor, puede ser una mála práctica de
seguridad. Lo mejor, desde mi punto de vista, es hacer un POST sobre la URI de
la colección. Opcionalmente con el POST podemos mandar el estado inicial de
la nueva entidad. Si hay éxito, el servidor responde con 201, y opcionalmente
con el estado de la nueva entidad en el cuerpo de la respuesta. En la respuesta
debe aparecer la cabecera Location indicando la URI de la nueva entidad. Esto
permite al servidor controlar las URIs de las entidades. De nuevo podemos
diseñar el servidor de forma que todas las entidades tengan un estado por
defecto inicial, de esta manera el cliente sólo necesita enviar los valores
iniciales de la nueva entidad que difieran del valor por defecto. Los mensajes
HTTP quedarían:
Petición de creación de una entidad:
POST https://www.mybooks.com/books HTTP/1.1
Content-Type: application/json
Accept: application/json,application/xml;q=0.5
{
'title':'Como hacerse rico rápido'
}
Respuesta exitosa, pero sin datos:
HTTP/1.1 201 Created
Location: https://www.mybooks.com/books/XSR9RFSV43r52
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 11/13
Respuesta exitosa, con datos:
HTTP/1.1 201 Created
Location: https://www.mybooks.com/books/XSR9RFSV43r52
Content-Type: application/json
{
'id':'XSR9RFSV43r52',
'title':'Como hacerse rico rápido',
'precio':'20euro' <-- Precio por defecto
}
Un detalle sobre todo lo anteriormente mencionado, no todas las entidades ni
colecciones tienen porqué admitir todos los métodos anteriormente descritos.
Podemos tener entidades de sólo lectura o colecciones que no admiten DELETE
o POST. Podemos llegar a tener casos de colecciones degeneradas que sólo
tienen un miembro y no admiten ni DELETE ni POST. En este caso podemos
prescindir totalmente de dicha colección, quedándonos con una entidad que no
está asociada a ninguna colección y que no se puede borrar. En estos casos
estamos ante el patrón singleton, que en el mundo de REST no es un
antipatrón. De hecho nos sirve para modelar cosas como enumeraciones, y más
cosas que ya veremos más adelante. La URI constructiva de un singleton se
define como: http(s)://www.myempresa.com/<sistema>/<singleton> Los
singletons pueden colgarse de otras colecciones, entidades y singletons. Ej.
https://www.bookstore.com/shop/countries/spain/zip-codes [6]
A veces nos encontramos con entidades que tiene un estado enorme, con
cientos de campos, o con campos que son arrays con cientos o miles de
posiciones. En estos caso transmitir el estado entero de la entidad puede ser
ineficaz. En estos casos mi consejo es que refactoriceis vuestras entidades. Al
igual que no es bueno tener clases monstruo con muchos métodos y varias
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 12/13
responsabilidades, no suele ser una buena señal de diseño tener una entidad
con un estado monstruo. Tal vez ese array enorme, es una colección, o esos 30
campos sean en realidad otra entidad.
Finalmente tenemos el patrón maestro/esclavo. En este patrón existen
recursos “esclavos” subordinados a otros recursos “maestros”. La identidad de
un recurso esclavo es dependiente de su recurso maestro, de esta forma para
localizar un recurso esclavo, necesitamos la identidad de su maestro. Además si
el recurso maestro es borrado, todos sus recursos esclavos son eliminados, con
lo que existe una dependencia en existencia. El uso de maestros y esclavos es
muy útil para modelar relaciones parte/todo o relaciones de composición. Como
recursos REST que son, tienen URI propia, pero desde un punto de vista
funcional no poseen una identidad independiente de su dueño, por lo tanto la
URI de un esclavo debería ser:
http(s)://www.myserver.com/<sistema>/<maestro>/<esclavo>. Ej. Para modelar
que cada usuario tiene una lista o carrito de la compra, podemos crear el recurso
esclavo shoppingList. En este caso shoppingList es esclavo de cada instancia de
la entidad users, con lo que la URI quedaría
https://www.mybooks.com/books/users/GOP34/shoppingList [7]
Es interesante notar, que el patrón entidad/colección, es un caso particular del
maestro/esclavo, donde la colección es el maestro y cada instancia de entidad
esclavo de la colección. Un fallo típico de diseño es el confundir subobjetos del
estado de un recurso, con un recurso esclavo, y viceversa. Los subobjetos del
estado de un recurso, no poseen ningún tipo de identidad, y no deben tener una
URI y por tanto no modelarse como recursos REST. Un ejemplo de subobjeto,
que no debe ser modelado como un recurso esclavo, serían las direcciones de un
usuario del sistema. Dos direcciones con los mismos datos, son exactamente
equivalentes, y por lo tanto no poseen identidad, ¿a que nos parecería raro una
lista de direcciones, con direcciones duplicadas?. Sin embargo en un supuesto
sistema de banca, donde cada usuario puede tener cuentas, cada cuenta es un
recurso en si mismo, aunque sea esclavo del usuario. Ciertamente podríamos
tener dos cuentas con el mismo saldo, pero no podrían considerarse
equivalentes, de aquí que cada una tenga una identidad diferente y una URI
propia.
Como veis el diseñar un CRUD es bastante sencillo si seguimos la filosofía REST
y aprovechamos HTTP. El diseño surge de forma tan natural que muchos recién
29/10/13 Servicios web (3): Diseñando servicios REST (1) | Te lo dije ...
eamodeorubio.wordpress.com/2010/09/03/servicios-web-3-disenando-servicios-rest-1/ 13/13
1. http://eamodeorubio.wordpress.com/author/eamodeorubio/
2. http://wp.me/pQAdW-4K
3. https://www.bookstore.com/shop/books
4. https://www.bookstore.com/shop/books/XSR9RFSV43r52
5. https://www.bookstore.com/shop/books?
page=4&tituloContiene=robots&precioMaximo=20euro
6. https://www.bookstore.com/shop/countries/spain/zip-codes
7. https://www.mybooks.com/books/users/GOP34/shoppingList
8. http://en.wordpress.com/about-these-ads/
llegados a REST cometen el error de pensar que REST es sólo una manera de
hacer CRUD sobre HTTP. No caigáis en esta tentación, ya que va a limitar
vuestros diseños de servicios REST y forzaros a pensar en “tablas” en vez de en
una web de objetos accesibles por internet.
Como prometí este era un post sencillito, que no estoy para muchos trotes ¡ En
el próximo post más !
ACTUALIZACION: Mi amigo Thorsten me ha señalado un par de erratas que he
corregido.
About these ads [8]