来自RESTful API的分页响应有效负载


83

我想在我的RESTful API中支持分页。

我的API方法应通过返回产品的JSON列表/products/index。但是,可能有成千上万种产品,我想分页浏览它们,因此我的请求应如下所示:

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

但是我的JSON响应需要什么样?API使用者通常会在响应中期望分页元数据吗?还是仅需要一系列产品?为什么?

看起来Twitter的API包含元数据: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"
  }
]

Answers:


110

ReSTful API主要由其他系统使用,这就是为什么我将页面数据放在响应头中。但是,某些API使用者可能无法直接访问响应标头,或者可能正在您的API上构建UX,因此提供一种(按需)检索JSON响应中的元数据的方法是一个加号。

我相信您的实现应包括机器可读的元数据作为默认值,并在请求时包括人类可读的元数据。如果您喜欢或最好通过查询参数(例如include=metadata或)按需提供,人类可读的元数据可以随每个请求一起返回include_metadata=true

在您的特定情况下,我将在记录中包括每个产品的URI。这使API使用者可以轻松地创建指向各个产品的链接。我还将根据我的分页请求的限制设置一些合理的期望。可接受并记录页面大小的默认设置。例如,GitHub的API将默认页面大小设置为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

(链接标头值应使用urlencoded)

...以及可能的自定义total-count响应标题(如果您选择):

total-count: 521

以人为中心的元数据中显示的其他分页数据对于以机器为中心的元数据可能是多余的,因为链接头使我知道我所在的页面以及每页的数目,并且我可以快速检索数组中的记录数。因此,我可能只会为总计数创建一个标题。您以后总是可以改变主意并添加更多元数据。

顺便说一句,您可能会注意到我已从/index您的URI中删除。一个普遍接受的约定是让您的ReST端点公开集合。有/index在年底muddies是略有上升。

这些只是我在使用/创建API时希望拥有的一些东西。希望有帮助!


per_page没有遵循page_size的约定
Alexandros Spyropoulos

1
"page_count": 20{"last": "/products?page=26&per_page=20"}
杰罗姆

1
如果从第1页到第x页获取所有记录时产品的数量突然增加,会发生什么?
MeV

3
@MeV在任何基于游标的分页方案中都会发生相同的情况:总数将增加,并且如果最后一页已满,则页面数可能会增加,仅此而已。在每个使用这种分页类型的应用程序上,这都是非常常见的情况。如果新产品出现在首页或最后一页,将取决于所使用的排序。
Pablo Pazos

2
“ ReSTful API主要由其他系统使用,这就是为什么我将页面数据放在响应头中”这就像在外面说晴天,这就是为什么我穿着一件蓝色衬衫的原因。是什么让您认为标头无法被人类读取?
更好的Oliver,

29

作为编写了多个使用REST服务的库的人,让我向您介绍客户为什么我认为将结果包装在元数据中的方法:

  • 如果没有总数,客户端如何知道它尚未收到所有内容,应该继续在结果集中进行分页?在未执行的UI中,请转到下一页,在最坏的情况下,这可能表示为“下一个/更多”链接,实际上并没有获取更多数据。
  • 在响应中包含元数据将允许客户端跟踪较少的状态。现在,我不必将我的REST请求与响应进行匹配,因为响应包含重构请求状态所需的元数据(在这种情况下,游标进入数据集)。
  • 如果状态是响应的一部分,那么我可以同时对同一个数据集执行多个请求,并且可以按碰巧到达的任何顺序处理请求,而这不一定是我发出请求的顺序。

还有一个建议:像Twitter API一样,您应该用一个直接的索引/光标替换page_number。原因是,API允许客户端设置每个请求的页面大小。返回的page_number是客户端到目前为止请求的页面数,还是给出最后使用的page_size的页面数(几乎可以肯定是后者,但是为什么不完全避免这种歧义)?


10
对于您的第一个项目符号,如果没有下一页,省略rel = next链接是否是合适的解决方案?对于第二个项目符号,该信息仍可在对客户端的响应中使用,它不位于响应的正文中,而是位于标头中。上一段+1。
凯尔·海斯

17

我建议添加相同的标题。将元数据移至标头有助于摆脱诸如或的信封result,并且响应主体仅包含我们需要的数据。如果您也生成分页链接,则可以使用Link标头。datarecords

    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

对于摇摇欲坠的文件:

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


2
根据RFC-6648,应在元数据密钥中删除“ X-”前缀。

1
@RayKoopa谢谢,我已经更新了github页面,但忘了更新此答案。
阿德南·卡米利

0

只需在您的后端API中将新属性添加到响应主体中即可。从示例.net核心:

[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 /:lastIdObtained /: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中,当我从下往上滚动时,我传递了零值,当我得到答案时,我设置了获得的最后一个ID的值,当我从上到下滑动时,我传递了我得到的最后一个注册ID。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.