Полезная нагрузка отклика пагинации от RESTful API

84

Я хочу поддерживать разбиение на страницы в моем RESTful API.

Мой метод API должен возвращать список продуктов в формате JSON через /products/index. Однако потенциально существуют тысячи продуктов, и я хочу пролистать их, поэтому мой запрос должен выглядеть примерно так:

/products/index?page_number=5&page_size=20

Но как должен выглядеть мой ответ JSON? Будут ли потребители API обычно ожидать в ответе метаданных разбиения на страницы? Или нужен только набор продуктов? Почему?

Похоже, что API Twitter включает метаданные: https://dev.twitter.com/docs/api/1/get/lists/members (см. Пример запроса).

С метаданными:

{
  "page_number": 5,
  "page_size": 20,
  "total_record_count": 521,
  "records": [
    {
      "id": 1,
      "name": "Widget #1"
    },
    {
      "id": 2,
      "name": "Widget #2"
    },
    {
      "id": 3,
      "name": "Widget #3"
    }
  ]
}

Просто набор товаров (без метаданных):

[
  {
    "id": 1,
    "name": "Widget #1"
  },
  {
    "id": 2,
    "name": "Widget #2"
  },
  {
    "id": 3,
    "name": "Widget #3"
  }
]
Чад Джонсон
источник

Ответы:

111

API-интерфейсы ReSTful в основном используются другими системами, поэтому я помещаю данные подкачки в заголовки ответов. Однако некоторые потребители API могут не иметь прямого доступа к заголовкам ответов или могут создавать UX поверх вашего API, поэтому предоставление способа извлечения (по запросу) метаданных в ответе JSON является плюсом.

Я считаю, что ваша реализация должна включать машиночитаемые метаданные по умолчанию и удобочитаемые метаданные по запросу. Удобочитаемые метаданные могут быть возвращены с каждым запросом, если хотите, или, предпочтительно, по запросу с помощью параметра запроса, такого как include=metadataилиinclude_metadata=true .

В вашем конкретном сценарии я бы включил URI для каждого продукта с записью. Это упрощает для потребителя API создание ссылок на отдельные продукты. Я бы также установил некоторые разумные ожидания относительно ограничений моих запросов на подкачку. Внедрение и документирование настроек по умолчанию для размера страницы является приемлемой практикой. Например, API GitHub устанавливает размер страницы по умолчанию на 30 записей, максимум 100, а также устанавливает ограничение скорости на количество запросов к API. Если ваш API имеет размер страницы по умолчанию, тогда в строке запроса можно просто указать индекс страницы.

В сценарии, удобочитаемом человеком, при переходе к /products?page=5&per_page=20&include=metadataадресу ответ может быть следующим:

{
  "_metadata": 
  {
      "page": 5,
      "per_page": 20,
      "page_count": 20,
      "total_count": 521,
      "Links": [
        {"self": "/products?page=5&per_page=20"},
        {"first": "/products?page=0&per_page=20"},
        {"previous": "/products?page=4&per_page=20"},
        {"next": "/products?page=6&per_page=20"},
        {"last": "/products?page=26&per_page=20"},
      ]
  },
  "records": [
    {
      "id": 1,
      "name": "Widget #1",
      "uri": "/products/1"
    },
    {
      "id": 2,
      "name": "Widget #2",
      "uri": "/products/2"
    },
    {
      "id": 3,
      "name": "Widget #3",
      "uri": "/products/3"
    }
  ]
}

Для машиночитаемых метаданных я бы добавил в ответ заголовки Link :

Link: </products?page=5&perPage=20>;rel=self,</products?page=0&perPage=20>;rel=first,</products?page=4&perPage=20>;rel=previous,</products?page=6&perPage=20>;rel=next,</products?page=26&perPage=20>;rel=last

(значение заголовка ссылки должно быть закодировано)

... и, возможно, настраиваемый total-countзаголовок ответа, если хотите:

total-count: 521

Другие данные подкачки, отображаемые в метаданных, ориентированных на человека, могут быть излишними для метаданных, ориентированных на машину, поскольку заголовки ссылок сообщают мне, на какой странице я нахожусь, и количество на странице, и я могу быстро получить количество записей в массиве . Поэтому я бы, вероятно, создал только заголовок для общего подсчета. Вы всегда можете передумать позже и добавить больше метаданных.

Кстати, вы можете заметить, что я удалил /indexиз вашего URI. Общепринятое соглашение заключается в том, чтобы ваша конечная точка ReST открывала коллекции. Наличие /indexв конце немного запутывает это.

Это всего лишь несколько вещей, которые мне нравятся при использовании / создании API. Надеюсь, это поможет!

код
источник
per_page не соответствует соглашению page_size
Александрос Спиропулос
1
"page_count": 20и {"last": "/products?page=26&per_page=20"}?
Jérôme
1
что произойдет, если количество товаров внезапно увеличится при извлечении всех записей со страницы 1 на страницу x?
МэВ
3
@MeV то же самое, что происходит в любом сценарии разбивки на страницы на основе курсора: общее количество увеличится, а количество страниц может увеличиться, если последняя страница заполнена, ни больше, ни меньше. Это очень распространенный сценарий для каждого приложения, использующего этот тип разбивки на страницы. Будет ли новый продукт отображаться на первой или последней странице, будет зависеть от используемой сортировки.
Пабло Пазос
2
«API-интерфейсы ReSTful используются в основном другими системами, поэтому я помещаю данные подкачки в заголовки ответов». Это все равно что сказать, что на улице солнечно, поэтому я ношу синюю рубашку. Почему вы думаете, что заголовки не могут быть прочитаны людьми?
a better oliver
29

Как человек, написавший несколько библиотек для использования REST-сервисов, позвольте мне представить вам точку зрения клиента на то, почему я считаю, что упаковка результата в метаданные - лучший способ:

  • Без общего количества, как клиент может узнать, что он еще не получил все, что есть, и должен продолжить поиск по набору результатов? В пользовательском интерфейсе, который не выполнял предварительный просмотр следующей страницы, в худшем случае это может быть представлено как ссылка Next / More, которая фактически не получает больше данных.
  • Включение метаданных в ответ позволяет клиенту отслеживать меньшее количество состояний. Теперь мне не нужно сопоставлять мой REST-запрос с ответом, поскольку ответ содержит метаданные, необходимые для восстановления состояния запроса (в данном случае курсор в наборе данных).
  • Если состояние является частью ответа, я могу выполнять несколько запросов к одному и тому же набору данных одновременно, и я могу обрабатывать запросы в любом порядке, в котором они приходят, что не обязательно в том порядке, в котором я делал запросы.

И предложение: как и в Twitter API , вы должны заменить page_number прямым индексом / курсором. Причина в том, что API позволяет клиенту устанавливать размер страницы для каждого запроса. Является ли возвращаемый page_number количеством страниц, запрошенных клиентом на данный момент, или номером страницы с учетом последнего использованного page_size (почти наверняка более позднего, но почему бы не избежать такой двусмысленности в целом)?

Majix
источник
10
Для вашего первого пункта, будет ли подходящим решением опустить ссылку rel = next, если следующей страницы не было? Что касается вашего второго пункта, информация по-прежнему доступна в ответе клиенту, просто она находится не в теле ответа, а в заголовках. +1 к вашему последнему абзацу.
Кайл Хейс
В конце хочу ответить на ваше предложение. Разбивка курсора на страницы - это не то же самое, что разбивка на страницы / смещения. У обоих есть свои плюсы и минусы, а также соображения производительности.
kjonsson
19

Я бы рекомендовал добавить заголовки для того же самого. Перемещение метаданных заголовков помогает в избавлении от огибающих , как result, dataили recordsи тело ответа содержит только данные , которые нам нужны. Вы можете использовать заголовок Link, если вы также создаете ссылки для пагинации.

    HTTP/1.1 200
    Pagination-Count: 100
    Pagination-Page: 5
    Pagination-Limit: 20
    Content-Type: application/json

    [
      {
        "id": 10,
        "name": "shirt",
        "color": "red",
        "price": "$23"
      },
      {
        "id": 11,
        "name": "shirt",
        "color": "blue",
        "price": "$25"
      }
    ]

Подробнее см .:

https://github.com/adnan-kamili/rest-api-response-format

Для файла swagger:

https://github.com/adnan-kamili/swagger-response-template

Аднан камили
источник
3
Согласно RFC-6648, префикс «X-» должен быть опущен в ключах метаданных.
Рэй
1
@RayKoopa спасибо, я обновил страницу github, но забыл обновить этот ответ.
аднан камили
0

просто добавьте новое свойство серверного API в тело ответа. из примера .net core:

[Authorize]
[HttpGet]
public async Task<IActionResult> GetUsers([FromQuery]UserParams userParams)
{
  var users = await _repo.GetUsers(userParams);
  var usersToReturn = _mapper.Map<IEnumerable<UserForListDto>>(users);


  // create new object and add into it total count param etc
  var UsersListResult = new
  {
    usersToReturn,
    currentPage = users.CurrentPage,
    pageSize = users.PageSize,
    totalCount = users.TotalCount,
    totalPages = users.TotalPages
  };

  return Ok(UsersListResult);
}

В ответе тела это выглядит так

{
"usersToReturn": [
    {
        "userId": 1,
        "username": "nancycaldwell@conjurica.com",
        "firstName": "Joann",
        "lastName": "Wilson",
        "city": "Armstrong",
        "phoneNumber": "+1 (893) 515-2172"
    },
    {
        "userId": 2,
        "username": "zelmasheppard@conjurica.com",
        "firstName": "Booth",
        "lastName": "Drake",
        "city": "Franks",
        "phoneNumber": "+1 (800) 493-2168"
    }
],
// metadata to pars in client side
"currentPage": 1,
"pageSize": 2,
"totalCount": 87,
"totalPages": 44

}

Давид Акобия
источник
-3

Как правило, я делаю простым способом, что бы то ни было, я создаю конечную точку restAPI, например, «localhost / api / method /: lastIdObhibited /: countDateToReturn» с этими параметрами, вы можете сделать это простым запросом. в сервисе, например. .сеть

jsonData function(lastIdObtained,countDatetoReturn){
'... write your code as you wish..'
and into select query make a filter
select top countDatetoreturn tt.id,tt.desc
 from tbANyThing tt
where id > lastIdObtained
order by id
}

В Ionic, когда я прокручиваю снизу вверх, я передаю нулевое значение, когда получаю ответ, я устанавливаю значение последнего полученного идентификатора, а когда я скользлю сверху вниз, я передаю последний полученный идентификатор регистрации

Дейвисон Сантос
источник