Penyaringan, pengurutan, dan paginasi

Bug endpoint daftar yang tidak disadari siapa pun sampai halaman 3.

Endpoint daftar terlihat sederhana: minta produk, dapat produk. Lalu daftarnya tumbuh melebihi yang muat dalam satu response, pengguna ingin daftarnya diurutkan berdasarkan harga, disaring berdasarkan kategori, dan dicari berdasarkan nama, dan setiap fitur itu adalah sepotong kecil kode dengan kasus tepinya sendiri. Bug pada daftar itu licik karena response-nya selalu terlihat masuk akal. Halaman berisi sepuluh produk yang salah tetaplah halaman berisi sepuluh produk.

Pelajaran ini memakai /api/Products, yang punya 20 produk di data awalnya, dan endpoint demo di bawah /api/paginationdocs. Parameter yang sama berfungsi di setiap daftar dalam API ini, dan referensi API menjelaskannya.

ParameterFungsinya di sini
limitUkuran halaman, 1 sampai 100. Tanpanya, Anda mendapat semuanya.
pageNomor halaman mulai dari 1, dipakai bersama limit.
offsetMelewati sejumlah data ini, sebagai alternatif page.
sort, orderMengurutkan berdasarkan field apa pun, asc (bawaan) atau desc.
qPencarian tanpa membedakan huruf besar-kecil di field teks seperti title dan description.
nama field apa punFilter persis tanpa membedakan huruf besar-kecil, seperti ?category=books.

Paginasi

Minta tiga produk per halaman dan lihat header-nya selain body:

GET/api/Products?limit=3&page=1

Dua puluh produk dalam halaman berisi tiga berarti tujuh halaman, dengan halaman terakhir berisi dua. Halaman terakhir adalah tempat pertama mencari bug, karena itulah satu-satunya kasus ketika halaman tidak penuh:

Terminal
$ curl -i "https://api.testingapis.com/api/Products?limit=3&page=7"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
x-page: 7
x-per-page: 3
x-total-count: 20
x-total-pages: 7

[{"id":19,"title":"Non-Slip Yoga Mat",...},{"id":20,"title":"Resistance Bands Set",...}]

Benar: tujuh halaman, dan yang terakhir berisi produk 19 dan 20. Bug klasik di sini adalah menghitung jumlah halaman dengan pembagian bilangan bulat (20 / 3 = 6), yang menyembunyikan dua produk terakhir dari siapa pun yang menelusuri halaman.

Nilai di luar rentang

Berikutnya, kirim nilai yang menurut dokumentasi tidak valid, atau tidak disebut sama sekali. Ini yang dilakukan API terhadapnya:

Hasil
?limit=3&page=8    200, x-page: 8, body []
?limit=3&page=0    200, x-page: 1, same as page 1
?limit=0           200, x-per-page: 1
?limit=-1          200, x-per-page: 1
?limit=101         200, x-per-page: 100
?limit=abc         200, x-per-page: 20 (limit ignored, all 20 returned)

Server tidak pernah menolak. Ia menyesuaikan nilai yang buruk ke nilai valid terdekat, atau mengabaikannya. Itu desain yang sah, dan ramah bagi klien, tetapi tes sebaiknya mencatatnya, karena alternatifnya (400 yang menjelaskan rentang yang valid) sama umumnya. Kasus yang berbahaya adalah saat penyesuaian itu menyembunyikan bug klien. Klien yang mengirim limit=abc karena kesalahan template mendapat ke-20 data di sini. Pada tabel berisi dua juta baris, perilaku yang sama berarti server mencoba mengembalikan semuanya.

Halaman yang melewati akhir mengembalikan daftar kosong dengan 200. Itulah yang dilakukan sebagian besar API, dan itulah yang diharapkan klien yang terus berulang sampai tidak mendapat apa-apa lagi. 404 untuk halaman kosong juga bisa dibenarkan. 500 tidak.

Tanpa celah, tanpa duplikat

Tes paginasi yang paling penting juga yang paling mudah dijelaskan: telusuri setiap halaman, kumpulkan ID-nya, dan periksa bahwa Anda mendapat setiap data tepat sekali. Bandingkan jumlah yang Anda kumpulkan dengan X-Total-Count.

JavaScript
async function allPages(path, limit) {
  const seen = [];
  for (let page = 1; ; page++) {
    const res = await fetch(`https://api.testingapis.com${path}?limit=${limit}&page=${page}`);
    const items = await res.json();
    if (items.length === 0) break;
    seen.push(...items.map((item) => item.id));
  }
  return seen;
}

const ids = await allPages("/api/Products", 3);
const total = 20; // from X-Total-Count
console.log(ids.length === total, new Set(ids).size === ids.length);

Jalankan dengan beberapa ukuran halaman: 1, 3, 7, dan satu yang lebih besar dari seluruh daftar. Kesalahan selisih satu dalam perhitungan offset cenderung muncul hanya untuk ukuran tertentu. Jika API mendukung page dan offset, periksa bahwa limit=5&page=3 dan limit=5&offset=10 mengembalikan data yang sama.

Tes ini hanya lulus dengan andal jika tidak ada yang mengubah data selama tes berjalan. Di server bersama seperti ini, seseorang mungkin saja mengubahnya. Itu membawa kita ke kelemahan utama nomor halaman, yang dibahas di bawah.

Pengurutan

Urutkan berdasarkan harga, menurun, dan periksa urutan seluruh hasil, bukan hanya item pertama:

GET/api/Products?sort=price&order=desc&limit=5

Lima yang pertama kembali sebagai 249.99, 199.99, 89.99, 89.99, 59.99. Produk 3 dan 8 berharga sama. Nilai yang sama penting untuk paginasi: jika server tidak memecah nilai sama dengan cara yang tetap, produk yang sama bisa muncul di bawah halaman 1 dan di atas halaman 2, atau tidak di keduanya. Sebagian besar database tidak menjanjikan apa pun tentang urutan baris yang setara kecuali kueri menambahkan kunci pengurutan kedua, biasanya ID.

Mengurutkan berdasarkan kategori menunjukkan cara API ini menanganinya. Di dalam setiap kategori, produk tetap berurutan menurut ID, baik naik maupun turun:

Hasil (id:kategori)
?sort=category
10:books 11:books 12:books 13:books 6:clothing 7:clothing 8:clothing 9:clothing
1:electronics 2:electronics 3:electronics 4:electronics 5:electronics 14:home ...

?sort=category&order=desc
18:sports 19:sports 20:sports 14:home 15:home 16:home 17:home 1:electronics ...

Itu stabil, dan itulah yang Anda inginkan. Untuk mengujinya, urutkan berdasarkan field dengan banyak duplikat, ambil daftarnya dua kali (dan telusuri per halaman dengan limit kecil), lalu periksa bahwa urutannya tidak pernah berubah.

Kasus pengurutan lain yang bisa dicoba pada API mana pun:

  • String dengan huruf besar-kecil campuran. Apakah "apple" berada sebelum atau sesudah "Banana"?
  • Angka yang disimpan sebagai string, sehingga "10" berada sebelum "9".
  • Data yang field pengurutannya null atau hilang. Apakah ia berada di awal, di akhir, atau menghilang?
  • Tanggal dalam zona waktu yang berbeda.

Parameter yang diabaikan

Apa yang terjadi saat Anda salah menulis parameter menimbulkan lebih banyak masalah daripada fitur mana pun itu sendiri:

Hasil
?sort=colour&limit=5            200, ids 1,2,3,4,5 (unsorted)
?sort=price&order=sideways      200, sorted ascending
?categry=books                  200, all 20 products
?rating.rate=4.3                200, all 20 products

Semuanya mengembalikan 200 dan daftar yang terlihat wajar. Pengurutan berdasarkan field yang tidak ada dibuang. Urutan sideways menjadi naik. Filter yang salah eja, categry, diabaikan, jadi klien meminta buku dan mendapat ke-20 produk. Penyaringan berdasarkan field bersarang dengan notasi titik tidak didukung, dan itu juga diabaikan.

Dalam praktiknya, filter yang salah eja adalah yang terburuk. Bayangkan layar yang menampilkan "pesanan untuk pelanggan 42", dibangun di atas parameter filter yang diganti namanya oleh seseorang. Setelah pergantian nama itu, setiap pelanggan melihat setiap pesanan, dan API mengembalikan 200 sepanjang waktu. Saat menguji filter, selalu sertakan kasus ketika filter seharusnya menyingkirkan sebagian besar data, dan periksa bahwa memang begitu. Tes yang hanya memeriksa "setiap produk yang dikembalikan ada di kategori books" juga lulus pada daftar kosong, jadi periksa jumlahnya juga.

Filter di API ini persis dan tidak membedakan huruf besar-kecil: ?category=Electronics mengembalikan lima produk elektronik, dan ?category=electron mengembalikan daftar kosong. Field boolean bekerja dengan cara yang sama:

GET/api/Products?inStock=false

Pencarian dengan q adalah pencocokan substring. Di sinilah encoding mulai penting, karena kata kunci pencarian berisi spasi, ampersand, tanda persen, dan tanda kutip, dan semuanya punya makna khusus dalam URL:

Hasil (id)
?q=YOGA              19
?q=yoga+mat          19
?q=yoga%20mat        19
?q=100%25            6   (description contains "100% organic cotton")
?q=%27               11,13   (titles and descriptions with an apostrophe)
?q=                  all 20
?q=a&q=b             []
?category=books&category=home    []

Spasi berfungsi ditulis sebagai + atau %20. % literal harus dikirim sebagai %25, dan ia dengan benar menemukan kaus berbahan "100% organic cotton". Pencarian tanda apostrof berfungsi dan mengembalikan dua buku, tanda kecil bahwa pencarian tidak membangun SQL dengan menempelkan string (pelajaran keamanan menjelaskan mengapa itu penting). Jika Anda mengirim spasi tanpa meng-encode-nya, curl menolak mengirim request sama sekali.

Dua baris terakhir layak dicatat. Mengulang parameter itu sah dalam URL, dan API memperlakukannya dengan cara berbeda: nilai pertama menang, nilai terakhir menang, kedua nilai digabung dengan OR, atau digabung menjadi satu string. Di sini, mengulang q atau category mengembalikan daftar kosong, yang menandakan nilainya digabung menjadi sesuatu seperti books,home yang tidak cocok dengan apa pun. Jika antarmuka produk Anda suatu saat membolehkan orang memilih dua kategori, di sinilah ada bug yang menunggu ditemukan.

Menggabungkan semuanya

Saring, urutkan, dan paginasi dalam satu request, lalu periksa bahwa semuanya diterapkan dalam urutan yang benar: saring dulu, lalu urutkan, lalu ambil halamannya.

GET/api/Products?category=electronics&sort=price&order=desc&limit=2&page=2

Halaman 2 berisi produk 3 dan 4 (89.99 dan 59.99), dan X-Total-Countbernilai 5, jumlah produk elektronik, bukan 20. Jika total mengabaikan filter, klien akan menampilkan "halaman 1 dari 10" untuk daftar yang hanya tiga halaman. Kesalahan itu umum, karena total sering dihitung oleh kueri terpisah yang lupa diperbarui seseorang.

Mengapa nomor halaman bermasalah

Nomor halaman dan offset mendeskripsikan posisi dalam daftar: "lewati 20, beri saya 10". Itu tidak masalah untuk data yang tidak berubah. Saat data berubah, posisinya bergeser:

  1. Anda mengambil halaman 1 (item 1 sampai 10) dari daftar yang diurutkan dari yang terbaru.
  2. Seseorang membuat item baru. Item itu masuk ke paling atas, dan semuanya bergeser turun satu.
  3. Anda mengambil halaman 2 (posisi 11 sampai 20). Posisi 11 kini adalah item yang tadinya nomor 10, jadi Anda melihatnya dua kali.

Menghapus item melakukan kebalikannya: semuanya bergeser naik dan satu item tidak pernah ditampilkan. Untuk feed berita, itu gangguan kecil. Untuk pekerjaan yang mengekspor semua faktur dengan menelusuri halaman, itu berarti ada faktur yang hilang dari ekspor dan tidak ada yang menyadarinya.

Paginasi cursor menghindari ini dengan mendeskripsikan di mana Anda berhenti, bukan seberapa jauh Anda sudah berjalan. Setiap response memberi Anda token untuk request berikutnya:

GET/api/pagination/cursor?limit=3
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
Response
{
  "object": "list",
  "url": "/api/pagination/cursor",
  "data": [
    { "id": 1, "name": "Smart Headset", "category": "electronics", "price": 42.01, "createdAt": "2026-01-01T21:00:00Z" },
    { "id": 2, "name": "Compact Speaker", "category": "outdoors", "price": 79.02, "createdAt": "2026-01-02T10:00:00Z" },
    { "id": 3, "name": "Deluxe Blender", "category": "office", "price": 116.03, "createdAt": "2026-01-02T23:00:00Z" }
  ],
  "has_more": true,
  "next_cursor": "eyJsYXN0SWQiOjN9"
}
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Anda mengirim balik next_cursor sebagai ?cursor= dan terus berlanjut sampai has_morebernilai false. Cursor yang tidak valid mendapat 400 yang jelas: "Invalid cursor. Pass the 'next_cursor' value from a previous response unchanged."

Paginasi keyset adalah ide yang sama tanpa pembungkus. Klien mengirim ID terakhir yang dilihatnya, dan server mengembalikan data setelahnya:

GET/api/pagination/keyset?after_id=10&limit=3
Request ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis
Response
{
  "data": [
    { "id": 11, "name": "Smart Lamp", "category": "electronics", "price": 12.11, "createdAt": "2026-01-07T07:00:00Z" },
    { "id": 12, "name": "Compact Headset", "category": "outdoors", "price": 49.12, "createdAt": "2026-01-07T20:00:00Z" },
    { "id": 13, "name": "Deluxe Speaker", "category": "office", "price": 86.13, "createdAt": "2026-01-08T09:00:00Z" }
  ],
  "has_more": true,
  "next_after_id": 13
}

Gaya ketiga menaruh tautan halaman di header Link, seperti yang dilakukan API GitHub. Body-nya tetap berupa array biasa:

Terminal
$ curl -i "https://api.testingapis.com/api/pagination/link?page=2&per_page=3"
HTTP/1.1 200 OK
link: <http://api.testingapis.com/api/pagination/link?page=3&per_page=3>; rel="next",
      <http://api.testingapis.com/api/pagination/link?page=34&per_page=3>; rel="last",
      <http://api.testingapis.com/api/pagination/link?page=1&per_page=3>; rel="first",
      <http://api.testingapis.com/api/pagination/link?page=1&per_page=3>; rel="prev"
x-total-count: 100
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Jumlah halamannya benar (100 item dalam halaman berisi 3 berarti 34 halaman). Tetapi lihat URL-nya. Request-nya dikirim ke https, dan setiap tautan menunjuk ke http. Ini bug nyata, dan umum: aplikasi berjalan di belakang proxy yang menangani HTTPS, ia sendiri hanya melihat HTTP biasa, dan membangun tautan dari apa yang dilihatnya. Klien yang mengikuti tautan ini akan dialihkan atau, pada konfigurasi yang lebih ketat, mengirim request berikutnya tanpa enkripsi. Memeriksa skema dan host dari setiap URL yang dihasilkan API adalah tes murah yang menangkap hal ini.

Untuk setiap gaya paginasi, tes intinya sama: telusuri sampai akhir, kumpulkan ID, cari celah dan duplikat, dan periksa apa yang terjadi di halaman terakhir. Untuk cursor dan keyset, tambahkan satu data selagi menelusuri dan pastikan Anda tetap melihat setiap data asli tepat sekali.

Latihan

Pilih /api/Booksmasuk untuk melihat dan catat hasil setiap langkah.

  1. Cari tahu berapa banyak buku dari X-Total-Count, lalu telusuri daftar dengan limit=4. Apakah Anda mendapat setiap ID sekali?
  2. Urutkan berdasarkan genre. Ambil dua kali dan dengan limit=3 melintasi halaman. Apakah urutan di dalam setiap genre stabil?
  3. Saring berdasarkan satu genre, urutkan berdasarkan price menurun, dan minta halaman 2. Apakah X-Total-Count adalah jumlah yang sudah disaring?
  4. Salah eja nama filternya. Apa yang kembali?
  5. Telusuri /api/pagination/offsetmasuk untuk melihat dan /api/pagination/linkmasuk untuk melihat sampai akhir. Di mana keduanya menandai halaman terakhir, dan apakah keduanya sepakat soal totalnya?

Untuk setiap kejutan, putuskan apakah itu bug, celah di dokumentasi, atau pilihan desain yang tetap akan Anda pertanyakan.

Berlatih dan melangkah lebih jauh