Foto oleh Santhosh Kanthala via Pexels

Pagination, filtering, dan sorting adalah tiga kemampuan penting yang harus dimiliki endpoint API yang mengembalikan koleksi data, terutama ketika jumlah data bisa bertumbuh besar seiring waktu dan mengembalikan seluruh data sekaligus akan sangat tidak efisien baik dari sisi performa maupun penggunaan bandwidth.

Pagination membatasi jumlah data yang dikembalikan per request, filtering memungkinkan client mengambil hanya subset data yang relevan sesuai kriteria tertentu, dan sorting memungkinkan client menentukan urutan data yang diterima sesuai kebutuhan tampilan mereka.

Artikel ini membahas implementasi ketiga kemampuan ini di ASP.NET Core Web API, termasuk offset-based pagination yang paling umum dipakai serta pertimbangan performa untuk dataset yang sangat besar.

Offset-Based Pagination dengan Page dan PageSize

Skip dan Take mengimplementasikan pagination berbasis halaman

[HttpGet] public IActionResult GetProduk(int halaman = 1, int ukuranHalaman = 10) { var data = _context.Produk .Skip((halaman - 1) * ukuranHalaman) .Take(ukuranHalaman) .ToList(); return Ok(new { data, halaman, totalData = _context.Produk.Count() }); } // GET /api/produk?halaman=2&ukuranHalaman=10
Parameter halaman dan ukuranHalaman memungkinkan client menentukan data pada halaman mana yang ingin diambil, dengan Skip dan Take dari LINQ (yang diterjemahkan EF Core menjadi OFFSET dan FETCH pada SQL) mengambil hanya sebagian data yang relevan, bukan seluruh tabel sekaligus.

Filtering dengan Query Parameter

Filter dibangun secara dinamis berdasarkan parameter yang diberikan

[HttpGet] public IActionResult GetProduk(string kategori = null, decimal? hargaMin = null) { var query = _context.Produk.AsQueryable(); if (!string.IsNullOrEmpty(kategori)) query = query.Where(p => p.Kategori == kategori); if (hargaMin.HasValue) query = query.Where(p => p.Harga >= hargaMin.Value); return Ok(query.ToList()); }
Membangun query secara dinamis dengan menambahkan kondisi Where hanya ketika parameter filter tersebut diberikan memungkinkan satu endpoint menangani berbagai kombinasi filter sekaligus, tanpa perlu membuat endpoint terpisah untuk setiap kombinasi kriteria filter yang mungkin diminta client.

Sorting dengan Parameter Dinamis

Switch expression memilih kolom pengurutan secara dinamis

[HttpGet] public IActionResult GetProduk(string urutkanBerdasarkan = "nama", bool menurun = false) { var query = _context.Produk.AsQueryable(); query = urutkanBerdasarkan switch { "harga" => menurun ? query.OrderByDescending(p => p.Harga) : query.OrderBy(p => p.Harga), _ => menurun ? query.OrderByDescending(p => p.Nama) : query.OrderBy(p => p.Nama) }; return Ok(query.ToList()); }
Parameter urutkanBerdasarkan dan menurun memungkinkan client menentukan kolom dan arah pengurutan data sesuai kebutuhan tampilan mereka, dengan switch expression memetakan nilai string parameter ke ekspresi OrderBy yang sesuai tanpa perlu endpoint terpisah untuk setiap kemungkinan kombinasi pengurutan.

Menyertakan Metadata Pagination di Response

Metadata pagination membantu client membangun navigasi halaman

{ "data": [ /* array produk */ ], "pagination": { "halamanSaatIni": 2, "totalHalaman": 15, "totalData": 150, "ukuranHalaman": 10 } }
Menyertakan metadata pagination seperti totalHalaman dan totalData dalam response membantu client membangun UI navigasi halaman (seperti tombol next/previous atau nomor halaman) tanpa perlu melakukan query terpisah hanya untuk mengetahui berapa total data yang tersedia secara keseluruhan.

Cursor-based Pagination sebagai Alternatif Offset Pagination

Offset pagination vs cursor-based pagination pada dataset yang terus bertambah

// Offset pagination: rawan data duplikat/terlewat jika data berubah GET /api/produk?page=3&limit=20 // SELECT * FROM Produk ORDER BY Id OFFSET 40 ROWS FETCH NEXT 20 ROWS ONLY // Cursor-based pagination: stabil walau data baru terus masuk GET /api/produk?cursor=eyJpZCI6MTIwfQ&limit=20 // SELECT * FROM Produk WHERE Id > 120 ORDER BY Id FETCH NEXT 20 ROWS ONLY // Response menyertakan cursor berikutnya: // { "data": [...], "nextCursor": "eyJpZCI6MTQwfQ" }

Offset pagination dengan parameter page dan limit mudah diimplementasikan, namun memiliki kelemahan ketika data terus berubah di antara dua permintaan halaman. Jika ada baris baru disisipkan sebelum offset yang sedang diambil, baris yang seharusnya muncul di halaman berikutnya bisa ikut bergeser, menyebabkan client melewatkan atau menerima data duplikat tanpa disadari.

Cursor-based pagination mengatasi masalah ini dengan menjadikan nilai unik dari baris terakhir yang sudah diambil, biasanya Id atau kombinasi timestamp dan Id, sebagai acuan untuk mengambil batch berikutnya. Karena query memfilter berdasarkan nilai cursor dan bukan posisi baris, penyisipan data baru di awal tabel tidak akan mempengaruhi urutan atau kelengkapan data yang sudah diambil pada halaman sebelumnya.

Trade-off dari cursor-based pagination adalah client tidak bisa langsung melompat ke halaman tertentu seperti 'halaman 10', karena akses data bersifat sekuensial mengikuti cursor. Pendekatan ini paling cocok untuk feed data yang terus bertambah seperti notifikasi atau log transaksi, sementara offset pagination masih relevan untuk tabel data yang relatif statis dan membutuhkan navigasi halaman bebas.

Filtering dengan Operator Query String dan Sorting Multi-Kolom

Query string dengan operator gte/lte dan sorting multi-kolom memakai tanda minus

// Filtering dengan operator perbandingan pada query string GET /api/produk?harga[gte]=50000&harga[lte]=200000&stok[gt]=0 // Diterjemahkan ke query SQL: // SELECT * FROM Produk // WHERE Harga >= 50000 AND Harga <= 200000 AND Stok > 0 // Sorting multi-kolom: urutkan kategori naik, lalu harga turun GET /api/produk?sort=kategori,-harga // ORDER BY Kategori ASC, Harga DESC

Filtering sederhana dengan kesetaraan seperti ?kategori=elektronik cukup untuk kasus dasar, namun kebutuhan nyata sering memerlukan perbandingan rentang nilai seperti harga minimum dan maksimum sekaligus. Konvensi umum menuliskan operator dalam kurung siku setelah nama field, misalnya harga[gte] untuk greater than or equal dan harga[lte] untuk less than or equal, yang kemudian diterjemahkan oleh server menjadi klausa WHERE dengan operator perbandingan yang sesuai. Operator seperti ne (not equal) dan in (daftar nilai) juga sering ditambahkan untuk mendukung filter yang lebih kompleks dari sisi client.

Sorting multi-kolom memungkinkan client menentukan lebih dari satu kriteria pengurutan sekaligus, dengan konvensi memisahkan nama kolom memakai koma dan menambahkan tanda minus di depan nama kolom untuk urutan menurun. Permintaan sort=kategori,-harga berarti data diurutkan berdasarkan kategori secara ascending terlebih dahulu, dan untuk kategori yang sama diurutkan lagi berdasarkan harga secara descending, mirip dengan klausa ORDER BY bertingkat pada SQL. Dokumentasi API perlu menjelaskan urutan prioritas sort secara eksplisit, agar konsumen API memahami kolom mana yang menjadi kriteria utama pengurutan.

Implementasi filtering dinamis seperti ini perlu divalidasi ketat di sisi server agar nama field dan operator yang diterima dibatasi pada daftar yang sudah ditentukan, bukan diteruskan mentah ke query builder. Tanpa validasi, mekanisme ini berisiko menjadi celah SQL injection atau memungkinkan client melakukan query terhadap kolom yang seharusnya tidak boleh diakses dari luar, seperti kolom harga modal yang bersifat internal. Pembatasan jumlah kolom yang boleh dipakai untuk filtering dan sorting juga membantu menjaga performa query tetap terkendali pada tabel berukuran besar.

Partial Response dengan Parameter Fields untuk Payload Lebih Ringkas

Query parameter fields memangkas kolom yang tidak diperlukan client

// Tanpa fields: seluruh kolom produk dikirim, termasuk yang tidak dipakai GET /api/produk/5 // { "id":5, "nama":"Kabel HDMI", "harga":45000, "stok":120, "deskripsi":"...", "berat":"..." } // Dengan fields: client memilih kolom yang benar-benar dibutuhkan GET /api/produk/5?fields=id,nama,harga // { "id": 5, "nama": "Kabel HDMI", "harga": 45000 }

Endpoint yang selalu mengembalikan seluruh kolom resource membebani bandwidth ketika client sebenarnya hanya butuh sebagian kecil data, misalnya tampilan daftar produk di mobile yang hanya perlu nama dan harga saja. Parameter query seperti fields memungkinkan client menentukan kolom mana saja yang ingin dikembalikan, sehingga server cukup memproyeksikan field yang diminta pada query database.

Implementasi di sisi server perlu membatasi nama kolom yang boleh diminta pada daftar yang memang ada di resource, agar permintaan fields yang keliru tidak menimbulkan error atau membocorkan kolom internal yang seharusnya tidak pernah diekspos. Pendekatan ini sangat membantu pada resource dengan banyak kolom opsional seperti deskripsi panjang atau data relasi, yang tidak selalu relevan untuk setiap tampilan client.

Kesalahan Umum yang Sering Terjadi

Mengembalikan Seluruh Data Tanpa Pagination Sama Sekali

Endpoint yang mengembalikan seluruh data tanpa batasan pagination akan menjadi sangat lambat dan boros bandwidth seiring data bertambah, serta berisiko membebani server jika jumlah data sudah mencapai ratusan ribu atau jutaan baris.

Melakukan Filtering di Memori Setelah Data Diambil

Mengambil seluruh data terlebih dahulu dengan ToList() baru kemudian memfilternya dengan LINQ di memori aplikasi, alih-alih menerapkan Where sebelum ToList, membuat EF Core mengambil data jauh lebih banyak dari yang sebenarnya dibutuhkan dari database.

Tidak Membatasi Nilai Maksimum PageSize

Tidak membatasi nilai maksimum ukuranHalaman yang bisa diminta client memungkinkan seseorang meminta ukuranHalaman=1000000, yang secara efektif menghilangkan manfaat pagination dan tetap bisa membebani server.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Offset-based pagination dengan Skip dan Take sederhana diimplementasikan dan dipahami, namun performanya menurun pada dataset sangat besar karena database tetap harus menghitung dan melompati baris-baris sebelumnya setiap kali.

Cursor-based pagination (menggunakan nilai unik terakhir sebagai acuan, bukan offset angka) memberikan performa lebih konsisten pada dataset besar, namun lebih kompleks diimplementasikan dan tidak mendukung lompat langsung ke halaman tertentu seperti yang bisa dilakukan offset-based pagination.

Ringkasan

Pagination, filtering, dan sorting adalah kemampuan esensial untuk endpoint API yang mengembalikan koleksi data, memastikan performa tetap baik dan client mendapatkan data yang relevan sesuai kebutuhan mereka. Selalu terapkan pagination sejak awal pada endpoint collection, dan batasi nilai parameter untuk mencegah penyalahgunaan yang bisa membebani server.

Seri Belajar REST API & Clean Architecture:

← Sebelumnya: Versioning API dan Kenapa Itu Penting

→ Selanjutnya: Pengenalan Clean Architecture pada Aplikasi Backend (segera terbit)