API yang mengirim seluruh data sekaligus adalah API yang belum pernah dipakai untuk data besar. Praktik nyata memecah respons menjadi halaman (pagination) dan membatasi jumlah permintaan per menit (rate limit) — dua mekanisme ini melindungi server, tetapi menuntut klien yang patuh. Program yang mengabaikan keduanya berakhir dengan data setengah jadi atau alamat IP yang diblokir. Pada Chapter ini kita membangun endpoint lokal yang menerapkan kedua mekanisme tersebut, lalu menulis klien yang menanganinya dengan benar.

Pagination Berbasis Halaman #

Pola paling umum memakai parameter nomor halaman: klien meminta ?halaman=1, server membalas potongan data beserta metadata jumlah halaman total. Klien tinggal mengulang permintaan sambil menaikkan nomor halaman sampai habis. Simpan server berikut sebagai server_artikel.py dan jalankan — lima artikel dipecah menjadi halaman berisi dua artikel:

python
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import parse_qs, urlsplit
import json

ARTIKEL = [
    {"id": 1, "judul": "Belajar Python Dasar"},
    {"id": 2, "judul": "Memahami HTTP"},
    {"id": 3, "judul": "Socket Programming"},
    {"id": 4, "judul": "REST API untuk Pemula"},
    {"id": 5, "judul": "Async dengan httpx"},
]
PER_HALAMAN = 2
PANGGILAN = 0


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        global PANGGILAN
        if self.path.startswith("/artikel"):
            query = parse_qs(urlsplit(self.path).query)
            halaman = int(query.get("halaman", ["1"])[0])
            mulai = (halaman - 1) * PER_HALAMAN
            potongan = ARTIKEL[mulai:mulai + PER_HALAMAN]
            payload = {
                "halaman": halaman,
                "total_halaman": 3,
                "data": potongan,
            }
            body = json.dumps(payload).encode("utf-8")
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        elif self.path == "/terbatas":
            PANGGILAN += 1
            if PANGGILAN == 1:
                self.send_response(429)
                self.send_header("Retry-After", "1")
                self.send_header("Content-Length", "0")
                self.end_headers()
                return
            body = b"akses diterima"
            self.send_response(200)
            self.send_header("X-RateLimit-Remaining", "59")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.send_header("Content-Length", "0")
            self.end_headers()

    def log_message(self, format, *args):
        pass


ThreadingHTTPServer(("127.0.0.1", 8123), Handler).serve_forever()

Menjelajah Semua Halaman #

Klien yang baik mengumpulkan seluruh halaman sampai metadata memberi sinyal selesai:

python
import requests

semua_artikel = []
halaman = 1

while True:
    respons = requests.get(
        "http://127.0.0.1:8123/artikel",
        params={"halaman": halaman},
        timeout=3,
    )
    data = respons.json()
    semua_artikel.extend(data["data"])
    print(f"Halaman {halaman}/{data['total_halaman']}: {len(data['data'])} artikel")
    if halaman >= data["total_halaman"]:
        break
    halaman += 1

print("Total artikel terkumpul:", len(semua_artikel))
for artikel in semua_artikel:
    print("-", artikel["judul"])

Output yang Dihasilkan:

text
Halaman 1/3: 2 artikel
Halaman 2/3: 2 artikel
Halaman 3/3: 1 artikel
Total artikel terkumpul: 5
- Belajar Python Dasar
- Memahami HTTP
- Socket Programming
- REST API untuk Pemula
- Async dengan httpx

Mari kita telaah logika pengumpulannya:

  1. Baris 3-4: List penampung dan penunjuk halaman disiapkan sebelum loop — loop while True akan menentukan sendiri kapan berhenti.
  2. Baris 7-11 (requests.get(..., params=...)): Nomor halaman dikirim sebagai query string; urutan permintaan tetap satu per satu, tidak serentak.
  3. Baris 13 (semua_artikel.extend(...)): Method .extend() menambahkan seluruh isi list data["data"] dari response ke list penampung — .append() yang keliru dipakai justru akan menyarangkan list di dalam list.
  4. Baris 15-17: Perbandingan halaman >= data["total_halaman"] menjadi kondisi berhenti; server yang mengirim total_halaman membuat loop tidak perlu menebak-nebak.

Page-Based dan Cursor-Based #

Pagination berbasis halaman bukan satu-satunya pola. API modern banyak yang beralih ke cursor-based:

AspekPage-based (?halaman=2)Cursor-based (?cursor=abc123)
Cara kerjaLompat ke nomor halamanLanjut dari penanda posisi terakhir
Data berubah di tengah jalanBisa terlewat atau dobelAman, penanda tetap konsisten
Bisa lompat acakYa (halaman=5 langsung)Tidak, harus berurutan
Ukuran respons serverLebih berat untuk data besarLebih ringan

Pola penulisan klien keduanya serupa: minta, periksa sinyal lanjut (nomor halaman berikutnya atau cursor pada respons), ulangi sampai sinyal itu habis.

Rate Limit dan Header X-RateLimit #

Server populer membatai jumlah permintaan per jendela waktu — misalnya 60 permintaan per menit. Sisa kuota biasanya diumumkan lewat header X-RateLimit-Remaining, dan saat kuota habis, server menjawab 429 Too Many Requests disertai header Retry-After yang menyatakan berapa detik kita harus menunggu. Route /terbatas pada server latihan kita meniru persis perilaku tersebut: permintaan pertama ditolak dengan 429 dan jeda 1 detik, permintaan kedua diterima.

python
import time

import requests

for percobaan in range(1, 4):
    respons = requests.get("http://127.0.0.1:8123/terbatas", timeout=3)
    if respons.status_code == 429:
        jeda = int(respons.headers["Retry-After"])
        print(f"429 — kebanyakan permintaan, tunggu {jeda} detik")
        time.sleep(jeda)
        continue
    print("200 — sisa kuota:", respons.headers["X-RateLimit-Remaining"])
    break

Output yang Dihasilkan:

text
429 — kebanyakan permintaan, tunggu 1 detik
200 — sisa kuota: 59

Alur klien di atas mengikuti kesepakatan yang diberikan server: baca Retry-After, tidur selama itu, lalu coba lagi — bukan memaksa menembak berulang yang justru memperpanjang hukuman. Guard range(1, 4) membatasi maksimal tiga percobaan sehingga program tidak berputar selamanya. Perhatikan juga bahwa pola ini menggabungkan dua pengetahuan dari chapter sebelumnya: pembacaan status code dari Chapter HTTP Request dan Response dan kesabaran terukur dari Chapter Timeouts dan Retries — batas kuota adalah “timeout” yang diumumkan server secara resmi.