¿Cuál es el mejor método RESTful para devolver el número total de elementos en un objeto?

139

Estoy desarrollando un servicio API REST para un gran sitio web de redes sociales en el que estoy involucrado. Hasta ahora, funciona muy bien. Puedo emitir GET, POST, PUTy DELETEpeticiones a las URL de objetos y afectar mis datos. Sin embargo, esta información está paginada (limitada a 30 resultados a la vez).

Sin embargo, ¿cuál sería la mejor manera RESTful de obtener el número total de miembros, digamos, a través de mi API?

Actualmente, publico solicitudes a una estructura de URL como la siguiente:

  • / api / members: devuelve una lista de miembros (30 a la vez como se mencionó anteriormente)
  • / api / members / 1: afecta a un solo miembro, según el método de solicitud utilizado

Mi pregunta es: ¿cómo utilizaría una estructura de URL similar para obtener el número total de miembros en mi aplicación? Obviamente, solicitar solo el idcampo (similar a Graph API de Facebook) y contar los resultados sería ineficaz dado que solo se devolverían una porción de 30 resultados.

Frijol Martin
fuente

Respuestas:

84

Si bien la respuesta a / API / usuarios está paginada y devuelve solo 30 registros, no hay nada que le impida incluir en la respuesta también el número total de registros y otra información relevante, como el tamaño de la página, el número de página / desplazamiento, etc. .

La API StackOverflow es un buen ejemplo de ese mismo diseño. Aquí está la documentación para el método de los usuarios: https://api.stackexchange.com/docs/users

Franci Penov
fuente
3
+1: Definitivamente lo más RESTful que hacer si se van a imponer límites de recuperación.
Donal Fellows
2
@bzim Sabrá que hay una página siguiente para buscar porque hay un enlace con rel = "next".
Darrel Miller el
44
@Donal el "próximo" rel está registrado en IANA iana.org/assignments/link-relations/link-relations.txt
Darrel Miller
1
@Darrel: sí, se podría hacer con cualquier tipo de indicador "siguiente" en la carga útil. Simplemente siento que tener el recuento total de los elementos de la colección en la respuesta es valioso en sí mismo y funciona como una "próxima" bandera de todos modos.
Franci Penov
55
Devolver un objeto que no es una lista de elementos no es una implementación adecuada de una API REST, pero REST no proporciona ninguna forma de obtener una lista parcial de resultados. Entonces, para respetar eso, creo que deberíamos usar encabezados para transmitir otras informaciones como el total, el token de la página siguiente y el token de la página anterior. Nunca lo intenté y necesito consejos de otros desarrolladores.
Loenix
74

Prefiero usar encabezados HTTP para este tipo de información contextual.

Para el número total de elementos, uso el X-total-countencabezado.
Para enlaces a la página siguiente, anterior, etc. Uso el Linkencabezado http :
http://www.w3.org/wiki/LinkHeader

Github lo hace de la misma manera: https://developer.github.com/v3/#pagination

En mi opinión, es más limpio, ya que se puede usar también cuando devuelve contenido que no admite hipervínculos (es decir, binarios, imágenes).

Ondrej Bozek
fuente
55
RFC6648 desprecia la convención de prefijar los nombres de parámetros no estandarizados con la cadena X-.
JDawg
70

Últimamente he estado haciendo una investigación exhaustiva sobre esta y otras preguntas relacionadas con la paginación REST y pensé que era constructivo agregar algunos de mis hallazgos aquí. Estoy ampliando la pregunta un poco para incluir pensamientos sobre paginación, así como el recuento, ya que están íntimamente relacionados.

Encabezados

Los metadatos de paginación se incluyen en la respuesta en forma de encabezados de respuesta. El gran beneficio de este enfoque es que la carga útil de respuesta en sí misma es solo el solicitante de datos real que estaba solicitando. Facilitar el procesamiento de la respuesta para los clientes que no están interesados ​​en la información de paginación.

Hay un montón de encabezados (estándar y personalizados) utilizados en la naturaleza para devolver información relacionada con la paginación, incluido el recuento total.

X-Total-Count

X-Total-Count: 234

Esto se usa en algunas API que encontré en la naturaleza. También hay paquetes NPM para agregar soporte para este encabezado, por ejemplo, para Loopback. Algunos artículos recomiendan configurar este encabezado también.

A menudo se usa en combinación con el Linkencabezado, que es una solución bastante buena para la paginación, pero carece de la información de recuento total.

Enlace

Link: </TheBook/chapter2>;
      rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
      </TheBook/chapter4>;
      rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Siento, al leer mucho sobre este tema, que el consenso general es usar el Linkencabezado para proporcionar enlaces de paginación a los clientes que usan rel=next, rel=previousetc. El problema con esto es que carece de la información de cuántos registros totales hay, que es por qué muchas API combinan esto con el X-Total-Countencabezado.

Alternativamente, algunas API y, por ejemplo, el estándar JsonApi , usan el Linkformato, pero agregan la información en un sobre de respuesta en lugar de un encabezado. Esto simplifica el acceso a los metadatos (y crea un lugar para agregar la información del recuento total) a expensas de aumentar la complejidad del acceso a los datos reales (agregando un sobre).

Rango de contenido

Content-Range: items 0-49/234

Promocionado por un artículo de blog llamado encabezado Range, ¡te elijo (para paginación)! . El autor presenta un argumento sólido para usar los encabezados Rangey Content-Rangepara la paginación. Cuando leemos cuidadosamente el RFC en estos encabezados, encontramos que extender el significado más allá de los rangos de bytes fue realmente anticipado por el RFC y está explícitamente permitido. Cuando se usa en el contexto de, en itemslugar de bytes, el encabezado Rango nos da una forma de solicitar un cierto rango de elementos e indicar a qué rango del resultado total se refieren los elementos de respuesta. Este encabezado también ofrece una excelente manera de mostrar el recuento total. Y es un verdadero estándar que se asigna principalmente uno a uno a la paginación. También se usa en la naturaleza .

Sobre

Muchas API, incluida la de nuestro sitio web favorito de preguntas y respuestas, usan un sobre , una envoltura alrededor de los datos que se utiliza para agregar metainformación sobre los datos. Además, OData estándares y JsonApi usan un sobre de respuesta.

La gran desventaja de esto (en mi humilde opinión) es que el procesamiento de los datos de respuesta se vuelve más complejo ya que los datos reales deben encontrarse en algún lugar del sobre. También hay muchos formatos diferentes para ese sobre y debe usar el correcto. Es revelador que los sobres de respuesta de OData y JsonApi son muy diferentes, con OData mezclando metadatos en múltiples puntos de la respuesta.

Punto final separado

Creo que esto se ha cubierto lo suficiente en las otras respuestas. No investigé tanto porque estoy de acuerdo con los comentarios de que esto es confuso ya que ahora tiene múltiples tipos de puntos finales. Creo que es mejor si cada punto final representa una (colección de) recurso (s).

Pensamientos adicionales

No solo tenemos que comunicar la metainformación de paginación relacionada con la respuesta, sino que también le permitimos al cliente solicitar páginas / rangos específicos. Es interesante observar también este aspecto para terminar con una solución coherente. Aquí también podemos usar encabezados (el Rangeencabezado parece muy adecuado) u otros mecanismos como los parámetros de consulta. Algunas personas abogan por el tratamiento de las páginas de resultados como recursos separados, que pueden tener sentido en algunos casos de uso (por ejemplo /books/231/pages/52. Terminé la selección de una gama salvaje de parámetros de la petición uso frecuente, tales como pagesize, page[size]y limitetc, además de apoyar la Rangecabecera (y como parámetro de la petición también).

Stijn de Witt
fuente
Estaba particularmente interesado en el Rangeencabezado, sin embargo, no pude encontrar suficiente evidencia de que usar algo aparte de bytesun tipo de rango sea válido.
VisioN
2
Creo que la evidencia más clara se puede encontrar en la sección 14.5 del RFC : acceptable-ranges = 1#range-unit | "none"creo que esta formulación deja explícitamente espacio para otras unidades de rango que bytes, aunque la especificación en sí misma solo define bytes.
Stijn de Witt
24

Alternativa cuando no necesita elementos reales

La respuesta de Franci Penov es sin duda la mejor manera de hacerlo, por lo que siempre devuelve elementos junto con todos los metadatos adicionales sobre las entidades que se solicitan. Así es como debe hacerse.

pero a veces devolver todos los datos no tiene sentido, porque es posible que no los necesite en absoluto. Quizás todo lo que necesita son esos metadatos sobre su recurso solicitado. Como recuento total o número de páginas o algo más. En tal caso, siempre puede hacer que la consulta de URL le indique a su servicio que no devuelva elementos, sino solo metadatos como:

/api/members?metaonly=true
/api/members?includeitems=0

o algo similar...

Robert Koritnik
fuente
10
Incrustar esta información en encabezados tiene la ventaja de que puede realizar una solicitud HEAD para obtener el recuento.
felixfbecker
1
@felixfbecker exactamente, gracias por reinventar la rueda y saturar las API con todo tipo de mecanismos diferentes :)
EralpB
1
@EralpB ¡Gracias por reinventar la rueda y saturar las API !? HEAD está escrito en HTTP. metaonlyo includeitemsno lo es.
felixfbecker
2
@felixfbecker solo "exactamente" fue para ti, el resto es para el OP. Perdón por la confusion.
EralpB
REST se trata de aprovechar HTTP y utilizarlo para lo que fue diseñado en la mayor medida posible. Content-Range (RFC7233) debe usarse en este caso. Las soluciones dentro del cuerpo no son buenas, especialmente porque no funcionará con HEAD. Crear nuevos encabezados como se sugiere aquí es innecesario y está mal.
Vance Shipley
23

Puede devolver el recuento como un encabezado HTTP personalizado en respuesta a una solicitud HEAD. De esta manera, si un cliente solo quiere el recuento, no necesita devolver la lista real, y no hay necesidad de una URL adicional.

(O, si se encuentra en un entorno controlado de punto final a punto final, puede usar un verbo HTTP personalizado como COUNT).

bzlm
fuente
44
¿"Encabezado HTTP personalizado"? Eso vendría bajo el título de ser algo sorprendente, lo que a su vez es contrario a lo que creo que debería ser una API RESTful. En última instancia, no debería ser sorprendente.
Donal Fellows
21
@Donal lo sé. Pero todas las buenas respuestas ya fueron tomadas. :(
bzlm
1
Yo también lo sé, pero a veces solo tienes que dejar que otras personas respondan. O haga su contribución mejor de otras maneras, como una explicación detallada de por qué debe hacerse de la mejor manera en lugar de otras.
Donal Fellows
44
En un entorno controlado, esto podría no ser sorprendente, ya que probablemente se usaría internamente y se basaría en la política API de sus desarrolladores. Yo diría que esta fue una buena solución en algunos casos y vale la pena tenerla aquí como una nota de una posible solución inusual.
James Billingham
1
Me gusta mucho usar encabezados HTTP para este tipo de cosas (es realmente a donde pertenece). El encabezado de enlace estándar podría ser apropiado en este caso (la API de Github lo usa).
Mike Marcacci
7

A partir de "X -" - El prefijo quedó en desuso. (ver: https://tools.ietf.org/html/rfc6648 )

Encontramos que "Accept-Ranges" es la mejor apuesta para mapear el rango de paginación: https://tools.ietf.org/html/rfc7233#section-2.3 Como las "Unidades de rango" pueden ser "bytes" o " simbólico". Ambos no representan un tipo de datos personalizado. (ver: https://tools.ietf.org/html/rfc7233#section-4.2 ) Aún así, se afirma que

Las implementaciones HTTP / 1.1 PUEDEN ignorar los rangos especificados usando otras unidades.

Lo que indica: el uso de unidades de rango personalizadas no está en contra del protocolo, pero PUEDE ser ignorado.

De esta manera, tendríamos que establecer los rangos de aceptación en "miembros" o cualquier tipo de unidad a distancia, es de esperar. Y además, también establezca Content-Range en el rango actual. (ver: https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.12 )

De cualquier manera, me apegaría a la recomendación de RFC7233 ( https://tools.ietf.org/html/rfc7233#page-8 ) para enviar un 206 en lugar de 200:

Si todas las condiciones previas son verdaderas, el servidor admite el
campo de encabezado Rango para el recurso de destino y los rangos especificados son
válidos y satisfactorios (como se define en la Sección 2.1), el servidor DEBE
enviar una respuesta 206 (Contenido parcial) con una carga útil que contiene una
o más representaciones parciales que corresponden a los
rangos satisfactorios solicitados, como se define en la Sección 4.

Entonces, como resultado, tendríamos los siguientes campos de encabezado HTTP:

Para contenido parcial:

206 Partial Content
Accept-Ranges: members
Content-Range: members 0-20/100

Para contenido completo:

200 OK
Accept-Ranges: members
Content-Range: members 0-20/20
Lepidopteron
fuente
3

Parece más fácil simplemente agregar un

GET
/api/members/count

y devolver el recuento total de miembros

willcodejavaforfood
fuente
11
No es Buena idea. Obliga a los clientes a hacer 2 solicitudes para construir la paginación en sus páginas. Primer pedido para obtener la lista de recursos y segundo para contar el total.
Jekis
Creo que es un buen enfoque ... también puede devolver solo una lista de resultados como json y en el lado del cliente verificar el tamaño de la colección, por lo que ese caso es un ejemplo estúpido ... además, puede tener / api / members / count y luego / api / members? offset = 10 & limit = 20
Michał Ziobro
1
También hay que tener en cuenta que una gran cantidad de tipos de paginación no requieren un recuento (Tales como desplazamiento infinito) - ¿Por qué el cálculo de este cuando el cliente no puede necesitarla
tofarr
2

¿Qué pasa con un nuevo punto final> / api / members / count que simplemente llama a Members.Count () y devuelve el resultado

Steve Woods
fuente
27
Darle al conteo un punto final explícito lo convierte en un recurso direccionable independiente. Funcionará, pero generará preguntas interesantes para cualquier persona nueva en su API. ¿Es el recuento de los miembros de la colección un recurso separado de la colección? ¿Puedo actualizarlo con una solicitud PUT? ¿Existe para una colección vacía o solo si hay elementos en ella? Si la memberscolección se puede crear mediante una solicitud POST a /api,/api/members/count se creará también como un efecto secundario, o tengo que hacer una solicitud POST explícita para crearla antes de solicitarla? :-)
Franci Penov
2

A veces, los marcos (como $ resource / AngularJS) requieren una matriz como resultado de la consulta, y realmente no puede tener una respuesta como {count:10,items:[...]}en este caso almaceno "count" en responseHeaders.

PD En realidad, puedes hacer eso con $ resource / AngularJS, pero necesita algunos ajustes.

Vahe Hovhannisyan
fuente
¿Cuáles son esos ajustes? Serían útiles en preguntas como esta: stackoverflow.com/questions/19140017/…
JBCP
Angular NO REQUIERE una matriz como resultado de la consulta, solo tiene que configurar su recurso con la propiedad de objeto de opción:isArray: false|true
Rémi Becheras
0

Podrías considerarlo countscomo un recurso. La URL sería entonces:

/api/counts/member
Frank Rem
fuente
-1

Al solicitar datos paginados, usted sabe (por valor de parámetro de tamaño de página explícito o valor de tamaño de página predeterminado) el tamaño de página, por lo que sabe si recibió todos los datos en respuesta o no. Cuando hay menos datos en respuesta que el tamaño de una página, entonces obtienes datos completos. Cuando se devuelve una página completa, debe volver a solicitar otra página.

Prefiero tener un punto final separado para el recuento (o el mismo punto final con el parámetro countOnly). Porque podría preparar al usuario final para un proceso que requiera mucho tiempo o tiempo mostrando una barra de progreso iniciada correctamente.

Si desea devolver el tamaño de datos en cada respuesta, también debe haber pageSize, offset mencionado. Para ser sincero, la mejor manera es repetir una solicitud de filtros también. Pero la respuesta se volvió muy compleja. Por lo tanto, prefiero un punto final dedicado para devolver el recuento.

<data>
  <originalRequest>
    <filter/>
    <filter/>
  </originalReqeust>
  <totalRecordCount/>
  <pageSize/>
  <offset/>
  <list>
     <item/>
     <item/>
  </list>
</data>

Couleage of mine, prefiere un parámetro countOnly al punto final existente. Entonces, cuando se especifica, la respuesta contiene solo metadatos.

punto final? filtro = valor

<data>
  <count/>
  <list>
    <item/>
    ...
  </list>
</data>

punto final? filter = value & countOnly = true

<data>
  <count/>
  <!-- empty list -->
  <list/>
</data>
Wooff
fuente