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:
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:
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:
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 httpxMari kita telaah logika pengumpulannya:
- Baris 3-4: List penampung dan penunjuk halaman disiapkan sebelum loop — loop
while Trueakan menentukan sendiri kapan berhenti. - Baris 7-11 (
requests.get(..., params=...)): Nomor halaman dikirim sebagai query string; urutan permintaan tetap satu per satu, tidak serentak. - Baris 13 (
semua_artikel.extend(...)): Method.extend()menambahkan seluruh isi listdata["data"]dari response ke list penampung —.append()yang keliru dipakai justru akan menyarangkan list di dalam list. - Baris 15-17: Perbandingan
halaman >= data["total_halaman"]menjadi kondisi berhenti; server yang mengirimtotal_halamanmembuat 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:
| Aspek | Page-based (?halaman=2) | Cursor-based (?cursor=abc123) |
|---|---|---|
| Cara kerja | Lompat ke nomor halaman | Lanjut dari penanda posisi terakhir |
| Data berubah di tengah jalan | Bisa terlewat atau dobel | Aman, penanda tetap konsisten |
| Bisa lompat acak | Ya (halaman=5 langsung) | Tidak, harus berurutan |
| Ukuran respons server | Lebih berat untuk data besar | Lebih 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.
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"])
breakOutput yang Dihasilkan:
429 — kebanyakan permintaan, tunggu 1 detik
200 — sisa kuota: 59Alur 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.