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

时间:2012-08-28 22:47:57

标签: rest pagination

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

我的API方法应该通过/products/index返回产品的JSON列表。但是,可能存在数千种产品,我想通过它们进行分页,所以我的请求应该是这样的:

/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"
  }
]

5 个答案:

答案 0 :(得分:85)

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

我相信您的实现应该包括默认的机器可读元数据,以及请求时的人类可读元数据。如果您愿意,可以随每个请求返回人类可读的元数据,或者最好通过查询参数(例如include=metadatainclude_metadata=true按需)返回。

在您的特定场景中,我会将每个产品的URI包含在记录中。这使API使用者可以轻松创建指向各个产品的链接。我还会根据我的分页请求的限制设置一些合理的期望。实现和记录页面大小的默认设置是可以接受的做法。例如,GitHub's 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 headers

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

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

另外,您可能会注意到我从您的URI中删除了/index。一个普遍接受的约定是让您的ReST端点公开集合。让/index结束时的混乱局面略有增加。

这些只是我在使用/创建API时喜欢的一些内容。希望有所帮助!

答案 1 :(得分:27)

作为编写了几个用于使用REST服务的库的人,让我向您提供客户视角,了解为什么我认为将结果包装在元数据中是可行的:

  • 如果没有总计数,客户怎么知道它还没有收到所有内容并且应该继续分页结果集?在没有预见到下一页的UI中,在最坏的情况下,这可能表示为实际上不再获取任何数据的Next / More链接。
  • 在响应中包含元数据允许客户端跟踪较少的状态。现在我不必将我的REST请求与响应匹配,因为响应包含重建请求状态所需的元数据(在这种情况下,光标到数据集中)。
  • 如果状态是响应的一部分,我可以同时对同一数据集执行多个请求,并且我可以按它们到达的任何顺序处理请求,这不一定是我发出请求的顺序。< / LI>

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

答案 2 :(得分:11)

我建议为此添加标题。将元数据移动到标题有助于摆脱resultdatarecords之类的信封,而响应正文只包含我们需要的数据。如果您也生成分页链接,则可以使用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

对于招摇文件:

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

答案 3 :(得分: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);
}

In body response it look like this

{
"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

}

答案 4 :(得分:-2)

通常,我通过简单的方法进行创建,无论如何,我都会创建一个restAPI端点,例如“ localhost / api / method /:lastIdObtained /:countDateToReturn” 使用这些参数,您可以执行一个简单的请求。 在服务中,例如.net

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的值,当我从顶部到底部滑动时,我通过了最后一个注册我知道了