¿Cómo diseñar una búsqueda / filtro RESTful?

Actualmente estoy diseñando e implementando una API RESTful en PHP. Sin embargo, no he tenido éxito implementando mi diseño inicial.

GET /users # list of users GET /user/1 # get user with id 1 POST /user # create new user PUT /user/1 # modify user with id 1 DELETE /user/1 # delete user with id 1 

Hasta ahora, bastante estándar, ¿verdad?

Mi problema es con el primero GET /users . Estaba considerando enviar parámetros en el cuerpo de la solicitud para filtrar la lista. Esto se debe a que quiero poder especificar filtros complejos sin obtener una url súper larga, como:

 GET /users?parameter1=value1&parameter2=value2&parameter3=value3&parameter4=value4 

En cambio, quería tener algo como:

 GET /users # Request body: { "parameter1": "value1", "parameter2": "value2", "parameter3": "value3", "parameter4": "value4" } 

que es mucho más legible y te ofrece grandes posibilidades para establecer filtros complejos.

De todos modos, file_get_contents('php://input') no devolvió el cuerpo de la solicitud para las solicitudes GET . También probé http_get_request_body() , pero el hosting compartido que estoy usando no tiene pecl_http . No estoy seguro de que hubiera ayudado de todos modos.

Encontré esta pregunta y me di cuenta de que GET probablemente no debería tener un cuerpo de solicitud. Fue un poco inconcluso, pero desaconsejaron.

Así que ahora no estoy seguro de qué hacer. ¿Cómo se diseña una función RESTful search / filterng?

Supongo que podría usar POST , pero eso no parece muy RESTful.

La mejor forma de implementar una búsqueda RESTful es considerar la búsqueda en sí misma como un recurso. Luego puede usar el verbo POST porque está creando una búsqueda. No es necesario crear literalmente algo en una base de datos para usar un POST.

Por ejemplo:

 Accept: application/json Content-Type: application/json POST http://example.com/people/searches { "terms": { "ssn": "123456789" }, "order": { ... }, ... } 

Está creando una búsqueda desde el punto de vista del usuario. Los detalles de implementación de esto son irrelevantes. Algunas API RESTful pueden no necesitar persistencia. Ese es un detalle de implementación.

Si utiliza el cuerpo de la solicitud en una solicitud GET, está rompiendo el principio REST, porque su solicitud GET no podrá almacenarse en caché, porque el sistema de caché usa solo la URL.

Y lo que es peor, su URL no se puede marcar, porque la URL no contiene toda la información necesaria para redirigir al usuario a esta página.

Use los parámetros de URL o de consulta en lugar de los parámetros de cuerpo de la solicitud.

p.ej:

 /myapp?var1=xxxx&var2=xxxx /myapp;var1=xxxx/resource;var2=xxxx 

De hecho, el HTTP RFC 7231 dice que:

Una carga útil dentro de un mensaje de solicitud GET no tiene semántica definida; enviar un cuerpo de carga útil en una solicitud GET puede causar que algunas implementaciones existentes rechacen la solicitud.

Para más información, mira aquí

Parece que el filtrado / búsqueda de recursos puede implementarse de manera RESTful. La idea es introducir un nuevo punto final llamado /filters/ or /api/filters/ .

El uso de este filtro de punto final se puede considerar como un recurso y, por lo tanto, se puede crear a través del método POST . De esta forma, por supuesto, el cuerpo se puede utilizar para transportar todos los parámetros y también se pueden crear estructuras de búsqueda / filtro complejas.

Después de crear dicho filtro, hay dos posibilidades para obtener el resultado de búsqueda / filtro.

  1. Se devolverá un nuevo recurso con ID único junto con el código de estado 201 Created . Luego, usando esta ID, se puede hacer una solicitud GET a /api/users/ like:

     GET /api/users/?filterId=1234-abcd 
  2. Después de crear un filtro nuevo a través de POST , no responderá con 201 Created pero a la vez con 303 SeeOther junto con el encabezado Location apunta a /api/users/?filterId=1234-abcd . Esta redirección se manejará automáticamente a través de la biblioteca subyacente.

En ambos casos, se deben realizar dos solicitudes para obtener los resultados filtrados; esto puede considerarse un inconveniente, especialmente para las aplicaciones móviles. Para las aplicaciones móviles, utilizaría una única llamada POST a /api/users/filter/ .

¿Cómo mantener los filtros creados?

Se pueden almacenar en DB y usar más adelante. También se pueden almacenar en algún almacenamiento temporal, por ejemplo, redis y tienen algo de TTL, después de lo cual caducarán y se eliminarán.

¿Cuáles son las ventajas de esta idea?

Los filtros, los resultados filtrados son almacenados en caché y pueden incluso marcarse como favoritos.

Creo que debe ir con los parámetros de solicitud, pero solo mientras no haya un encabezado HTTP apropiado para lograr lo que desea hacer. La especificación HTTP no dice explícitamente que GET no puede tener un cuerpo. Sin embargo, este artículo dice:

Por convención, cuando se utiliza el método GET, toda la información requerida para identificar el recurso está codificada en el URI. No existe una convención en HTTP / 1.1 para una interacción segura (por ejemplo, recuperación) donde el cliente proporciona datos al servidor en un cuerpo de entidad HTTP en lugar de en la parte de consulta de un URI. Esto significa que para operaciones seguras, los URI pueden ser largos.

No te preocupes demasiado si tu API inicial es completamente RESTANTE o no (especialmente cuando estás recién en las etapas alfa). Obtenga la plomería de back-end para que funcione primero. Siempre puede hacer algún tipo de transformación / reescritura de URL para trazar un mapa, perfeccionando iterativamente hasta que obtenga algo lo suficientemente estable como para realizar pruebas extensas (“beta”).

Puede definir URI cuyos parámetros están codificados por posición y convención en los URI mismos, con el prefijo de una ruta que sabe que siempre se asignará a algo. No conozco PHP, pero supongo que existe tal facilidad (como existe en otros lenguajes con marcos web):

.es decir. Haga un tipo de búsqueda de “usuario” con param [i] = valor [i] para i = 1..4 en la tienda n. ° 1 (con valor1, valor2, valor3, … como una abreviación para los parámetros de consulta de URI):

 1) GET /store1/search/user/value1,value2,value3,value4 

o

 2) GET /store1/search/user,value1,value2,value3,value4 

o de la siguiente manera (aunque yo no lo recomendaría, más sobre eso más adelante)

 3) GET /search/store1,user,value1,value2,value3,value4 

Con la opción 1, correlaciona todos los URI con prefijo /store1/search/user al manejador de búsqueda (o cualquiera que sea la designación de PHP) de forma predeterminada haciendo búsquedas de recursos en store1 (equivalente a /search?location=store1&type=user .

Por convención documentada y aplicada por la API, los parámetros de los valores 1 a 4 están separados por comas y se presentan en ese orden.

La opción 2 agrega el tipo de búsqueda (en este caso, user ) como parámetro posicional # 1. Cualquiera de las opciones es solo una opción estética.

La opción 3 también es posible, pero no creo que me gustaría. Creo que la capacidad de búsqueda dentro de ciertos recursos debe presentarse en el URI mismo antes de la búsqueda en sí (como si se indicara claramente en el URI que la búsqueda es específica dentro del recurso).

La ventaja de este sobrepaso de parámetros en el URI es que la búsqueda es parte del URI (por lo tanto, trata una búsqueda como un recurso, un recurso cuyo contenido puede cambiar, y lo hará, con el tiempo). La desventaja es que el orden de los parámetros es obligatorio .

Una vez que haga algo como esto, puede usar GET, y sería un recurso de solo lectura (ya que no puede POST o PUT a él, se actualiza cuando se GET). También sería un recurso que solo llega a existir cuando se invoca.

También se podría agregar más semántica mediante el almacenamiento en caché de los resultados durante un período de tiempo o con un DELETE que provoque la eliminación de la memoria caché. Sin embargo, esto puede ir en contra de lo que la gente suele usar para DELETE (y porque la gente suele controlar el almacenamiento en caché con encabezados de almacenamiento en caché).

Cómo lo harías sería una decisión de diseño, pero esta sería la forma en que lo haría. No es perfecto, y estoy seguro de que habrá casos en los que hacer esto no sea lo mejor que se pueda hacer (especialmente para criterios de búsqueda muy complejos).

Como estoy usando un backend laravel / php , tiendo a ir con algo como esto:

 /resource?filters[status_id]=1&filters[city]=Sydney&page=2&include=relatedResource 

PHP convierte automáticamente [] params en una matriz, por lo que en este ejemplo terminaré con una variable $filter que contiene una matriz / objeto de filtros, junto con una página y todos los recursos relacionados que deseo ansiosamente cargados.

Si usa otro idioma, esta podría ser una buena convención y puede crear un analizador para convertir [] a una matriz.

FYI: Sé que esto es un poco tarde, pero para cualquiera que esté interesado. Depende de qué tan RESTful quieras ser, tendrás que implementar tus propias estrategias de filtrado ya que las especificaciones HTTP no son muy claras al respecto. Me gustaría sugerir la encoding url de todos los parámetros de filtro, por ejemplo

 GET api/users?filter=param1%3Dvalue1%26param2%3Dvalue2 

Sé que es feo, pero creo que es la manera más RESTANTE de hacerlo y debería ser fácil de analizar por el lado del servidor 🙂