Contoh paginasi API: offset, halaman, cursor, keyset, header Link, dan HAL
Enam gaya paginasi atas data yang sama, masing-masing dengan response sungguhan dan loop yang mengambil setiap halaman.
Contoh terakhir dijalankan terhadap API pada
API membagi daftar panjang menjadi halaman dengan enam cara yang umum. Offset dan limit melewati sejumlah item. Nomor dan ukuran halaman menghitung halaman, seperti yang dilakukan Spring Data. Cursor adalah token buram, seperti yang dipakai Stripe, dan paginasi keyset meminta item setelah id terakhir. Sebagian API menaruh URL berikutnya di header Link, seperti GitHub, dan HAL menaruhnya di tautan hypermedia. https://api.testingapis.com/api/paginationmasuk untuk melihat menyajikan 100 item yang sama dalam keenam gaya, sehingga Anda bisa membandingkan response berdampingan dan menguji klien terhadap masing-masing. Gratis dan tanpa key. Setiap gaya ada di bawah dengan response sungguhan.
Keenam gaya sekilas
| Gaya | Request | Halaman berikutnya berasal dari | Dipakai oleh |
|---|---|---|---|
| Offset / limit | /offset?offset=20&limit=10 | offset + limit | Banyak API berbasis SQL |
| Nomor halaman (mulai 0) | /page?page=2&size=10 | number + 1, sampai last bernilai true | Spring Data |
| Cursor | /cursor?limit=10&cursor=… | next_cursor, sampai has_more bernilai false | Stripe |
| Keyset | /keyset?after_id=20&limit=10 | next_after_id | Tabel besar, feed |
| Header Link (mulai 1) | /link?page=3&per_page=10 | URL rel="next" di header Link | GitHub |
| HAL | /hal?page=2&size=10 | _links.next.href | Spring HATEOAS |
Semua path ada di bawah /api/paginationmasuk untuk melihat. Ukuran halaman berkisar 1 sampai 100, dengan default 10.
Offset dan limit
Gaya paling sederhana: lewati offset item dan kembalikan limit item berikutnya. Totalnya ada di body dan di X-Total-Count.
$ curl -i "https://api.testingapis.com/api/pagination/offset?offset=0&limit=2"
HTTP/1.1 200 OK
x-total-count: 100
{"data":[{"id":1,"name":"Smart Headset",...},{"id":2,"name":"Compact Speaker",...}],
"pagination":{"offset":0,"limit":2,"total":100,"hasMore":true}}Nomor dan ukuran halaman
Envelope milik Spring Data. Halaman dimulai dari 0, dan itulah bug yang perlu diuji: klien yang ditulis untuk halaman mulai 1 diam-diam melewati halaman pertama.
$ curl "https://api.testingapis.com/api/pagination/page?page=0&size=2"
{"content":[{"id":1,...},{"id":2,...}],
"pageable":{"pageNumber":0,"pageSize":2,"offset":0,"paged":true},
"totalElements":100,"totalPages":50,"number":0,"size":2,"numberOfElements":2,
"first":true,"last":false,"empty":false}Cursor
Response membawa next_cursor. Kirim kembali tanpa diubah untuk mendapatkan halaman berikutnya, dan berhenti saat has_more bernilai false. Cursor-nya kebetulan berupa JSON yang di-encode base64, tetapi klien harus memperlakukannya sebagai buram. Cursor yang diubah mendapat 400:
$ curl "https://api.testingapis.com/api/pagination/cursor?limit=2"
{"object":"list","url":"/api/pagination/cursor",
"data":[{"id":1,...},{"id":2,...}],
"has_more":true,"next_cursor":"eyJsYXN0SWQiOjJ9"}
$ curl "https://api.testingapis.com/api/pagination/cursor?cursor=garbage"
HTTP/1.1 400 Bad Request
{"status":400,"error":"Bad Request","message":"Invalid cursor. Pass the 'next_cursor' value from a previous response unchanged."}/api/pagination/cursor?limit=3Keyset
Mirip cursor, tetapi bisa dibaca: minta item dengan id lebih besar dari yang terakhir Anda lihat. Penambahan dan penghapusan di halaman sebelumnya tidak bisa menggeser hasilnya, itulah sebabnya feed dan tabel yang sangat besar memakainya.
$ curl "https://api.testingapis.com/api/pagination/keyset?after_id=10&limit=2"
{"data":[{"id":11,"name":"Smart Lamp",...},{"id":12,"name":"Compact Headset",...}],
"has_more":true,"next_after_id":12}Header Link
Body-nya berupa array biasa, dan navigasinya ada di header Link (RFC 8288), dengan URL absolut untuk next, prev, first, dan last. Halaman yang melewati akhir mengembalikan array kosong, bukan 404.
$ curl -i "https://api.testingapis.com/api/pagination/link?page=2&per_page=2"
HTTP/1.1 200 OK
link: <https://api.testingapis.com/api/pagination/link?page=3&per_page=2>; rel="next",
<https://api.testingapis.com/api/pagination/link?page=50&per_page=2>; rel="last",
<https://api.testingapis.com/api/pagination/link?page=1&per_page=2>; rel="first",
<https://api.testingapis.com/api/pagination/link?page=1&per_page=2>; rel="prev"
x-total-count: 100
[{"id":3,"name":"Deluxe Blender",...},{"id":4,"name":"Eco Bottle",...}]HAL
Item berada di bawah _embedded.items, masing-masing dengan tautan self-nya sendiri, dan tautan halaman ada di bawah _links. Content type-nya application/hal+json, yang tidak di-parse sebagai JSON oleh sebagian klien kecuali diberi tahu.
$ curl "https://api.testingapis.com/api/pagination/hal?page=0&size=2"
{"_embedded":{"items":[
{"id":1,...,"_links":{"self":{"href":"/api/pagination/items/1"}}},
{"id":2,...,"_links":{"self":{"href":"/api/pagination/items/2"}}}]},
"_links":{"self":{"href":"/api/pagination/hal?page=0&size=2"},
"first":{"href":"/api/pagination/hal?page=0&size=2"},
"next":{"href":"/api/pagination/hal?page=1&size=2"},
"last":{"href":"/api/pagination/hal?page=49&size=2"}},
"page":{"size":2,"totalElements":100,"totalPages":50,"number":0}}Mengambil setiap halaman
Sebagian besar bug paginasi muncul saat klien menelusuri seluruh daftar: berhenti satu halaman terlalu cepat, berputar tanpa akhir di halaman terakhir, atau menghitung halaman dari angka yang salah. Skrip ini mengambil ke-100 item dua kali, sekali lewat cursor dan sekali lewat header Link. Keduanya berakhir di id 100, jadi tidak ada yang terlewat atau terulang.
const BASE = "https://api.testingapis.com/api/pagination";
// Cursor: follow next_cursor until has_more is false.
async function allByCursor(limit = 25) {
const items = [];
let cursor;
do {
const url = new URL(`${BASE}/cursor`);
url.searchParams.set("limit", limit);
if (cursor) url.searchParams.set("cursor", cursor);
const page = await (await fetch(url)).json();
items.push(...page.data);
cursor = page.has_more ? page.next_cursor : undefined;
} while (cursor);
return items;
}
// Link header: follow rel="next" until there is none.
async function allByLinkHeader(perPage = 25) {
const items = [];
let url = `${BASE}/link?per_page=${perPage}`;
while (url) {
const res = await fetch(url);
items.push(...(await res.json()));
const next = res.headers.get("link")?.match(/<([^>]+)>;\s*rel="next"/);
url = next?.[1];
}
return items;
}
const byCursor = await allByCursor();
const byLink = await allByLinkHeader();
console.log("cursor:", byCursor.length, "items, last id", byCursor.at(-1).id);
console.log("link: ", byLink.length, "items, last id", byLink.at(-1).id);cursor: 100 items, last id 100
link: 100 items, last id 100Gaya mana yang dipilih
- Offset atau nomor halaman saat pengguna melompat ke halaman N dan datanya berubah perlahan. Baris yang ditambah atau dihapus di antara dua request menggeser setiap halaman berikutnya, sehingga sebuah item bisa terlewat atau muncul dua kali.
- Cursor atau keyset untuk feed, infinite scroll, dan ekspor, di mana data berubah saat seseorang menelusuri halaman. Anda tidak bisa langsung melompat ke halaman 40.
- Header Link atau HAL saat Anda ingin klien mengikuti URL yang diberikan server alih-alih menyusun sendiri. Server lalu bisa mengubah skema paginasinya tanpa merusak klien.