Koleksi tugas yang berisi sepuluh data memang enak dilihat sekaligus; koleksi yang berisi sepuluh ribu tidak. Endpoint yang mengembalikan seluruh isi tanpa batas membuat response membengkak, klien lambat, dan database bekerja lebih keras dari yang diperlukan — sementara klien sebenarnya hanya butuh satu halaman sekilas. Tiga teknik standar menyelesaikan masalah ini lewat query string: pagination membatasi jumlah data per permintaan, filtering menyaring sesuai kondisi, dan sorting mengatur urutan. Ketiganya kita terapkan sekaligus pada endpoint GET /tugas.

Parameter Query untuk Daftar #

Desain parameter mengikuti kebiasaan API modern:

ParameterDefaultMaknaContoh pemanggilan
page1nomor halaman yang diminta?page=2
limit5jumlah maksimum data per halaman?limit=20
selesaitidak adafilter status tugas?selesai=false
sortidfield pengurutan; awalan - berarti menurun?sort=-judul

Konvensi sort=-judul berasal dari dunia API publik: tanda minus di depan nama field berarti urutan menurun (Z ke A, besar ke kecil), tanpa tanda berarti menaik. Satu parameter, dua arah — lebih ringkas daripada sort=judul&order=desc.

Endpoint Paginasi Lengkap #

python
import json

from fastapi import FastAPI
from fastapi.testclient import TestClient

app = FastAPI()

tugas_db = [
    {"id": 1, "judul": "Belajar Python", "selesai": True},
    {"id": 2, "judul": "Belajar FastAPI", "selesai": False},
    {"id": 3, "judul": "Belajar REST", "selesai": True},
    {"id": 4, "judul": "Belajar Pydantic", "selesai": False},
    {"id": 5, "judul": "Belajar OpenAPI", "selesai": True},
    {"id": 6, "judul": "Belajar JWT", "selesai": False},
    {"id": 7, "judul": "Belajar Testing", "selesai": True},
    {"id": 8, "judul": "Belajar Versioning", "selesai": False},
]


@app.get("/tugas")
def daftar_tugas(
    page: int = 1,
    limit: int = 5,
    selesai: bool | None = None,
    sort: str = "id",
):
    hasil = tugas_db
    if selesai is not None:
        hasil = [t for t in hasil if t["selesai"] == selesai]

    desc = sort.startswith("-")
    kolom = sort.lstrip("-")
    hasil = sorted(hasil, key=lambda t: t[kolom], reverse=desc)

    total = len(hasil)
    mulai = (page - 1) * limit
    data = hasil[mulai : mulai + limit]
    return {
        "data": data,
        "meta": {
            "total": total,
            "page": page,
            "limit": limit,
            "has_next": mulai + limit < total,
        },
    }


client = TestClient(app)

r = client.get("/tugas")
print(json.dumps(r.json(), ensure_ascii=False, indent=2))

r = client.get("/tugas?page=2")
print([t["judul"] for t in r.json()["data"]], r.json()["meta"])

r = client.get("/tugas?selesai=false&sort=-judul&limit=2")
print([t["judul"] for t in r.json()["data"]], r.json()["meta"])

r = client.get("/tugas?sort=judul&limit=3&page=1")
print([t["judul"] for t in r.json()["data"]], r.json()["meta"])

Output yang Dihasilkan:

text
{
  "data": [
    {
      "id": 1,
      "judul": "Belajar Python",
      "selesai": true
    },
    {
      "id": 2,
      "judul": "Belajar FastAPI",
      "selesai": false
    },
    {
      "id": 3,
      "judul": "Belajar REST",
      "selesai": true
    },
    {
      "id": 4,
      "judul": "Belajar Pydantic",
      "selesai": false
    },
    {
      "id": 5,
      "judul": "Belajar OpenAPI",
      "selesai": true
    }
  ],
  "meta": {
    "total": 8,
    "page": 1,
    "limit": 5,
    "has_next": true
  }
}
['Belajar JWT', 'Belajar Testing', 'Belajar Versioning'] {'total': 8, 'page': 2, 'limit': 5, 'has_next': False}
['Belajar Versioning', 'Belajar Pydantic'] {'total': 4, 'page': 1, 'limit': 2, 'has_next': True}
['Belajar FastAPI', 'Belajar JWT', 'Belajar OpenAPI'] {'total': 8, 'page': 1, 'limit': 3, 'has_next': True}

Mari kita telaah alurnya:

  1. Baris 22-25 (page: int = 1, ...): keempat parameter pagination adalah query parameter biasa — nilai default membuat klien cukup memanggil GET /tugas tanpa query string sama sekali.
  2. Baris 28-29 (if selesai is not None:): filter memakai bool | None sehingga None berarti “tanpa filter”; ?selesai=false menghasilkan list comprehension yang hanya menyisakan tugas yang belum selesai.
  3. Baris 31-33 (desc = sort.startswith("-")): tanda minus dibaca lalu dibuang dari kolom — "-judul" menjadi desc=True, kolom="judul" — sebelum sorted() mengurutkan dengan key=lambda t: t[kolom].
  4. Baris 35-37 (mulai = (page - 1) * limit): halaman dikonversi menjadi slice; page=2, limit=5 memotong indeks 5 sampai 9. Inilah offset pagination — alternatifnya skip/limit langsung seperti pada chapter Dependency Injection.
  5. Baris 38-46 (return {...}): response dibungkus menjadi data plus meta — jangan pernah mengembalikan list telanjang bila endpoint terpaginasi, karena klien kehilangan tempat menaruh metadata.

Metadata dalam Response #

Tiga baris output terakhir memperlihatkan kombinasi yang sesungguhnya: ?selesai=false&sort=-judul&limit=2 menyaring empat tugas belum selesai, mengurutkan menurun berdasarkan judul, lalu memotong dua data pertama — dan meta tetap jujur (total: 4, has_next: True) karena keduanya dihitung sebelum pemotongan. Berkat total dan has_next, klien tahu kapan berhenti meminta halaman berikutnya tanpa harus menebak.

Endpoint daftar kini layak tampil; tinggal satu pintu yang masih terbuka lebar — siapa pun bisa memanggil dan mengubah data. Chapter Authentication dan Authorization menutupnya.