File CSV dan Excel yang kita pelajari pada chapter sebelumnya bersifat statis: isinya berhenti pada saat file tersebut dibuat. Padahal banyak data paling berguna justru terus berubah — cuaca, kurs valuta, stok gudang, hingga jumlah pemesanan hari ini. Data seperti itu biasanya disajikan melalui API, yaitu layanan yang memungkinkan program kita meminta data langsung dari server penyedianya. Pada chapter ini kita memakai library requests untuk mengambil data dari REST API: mulai dari permintaan GET paling sederhana, parameter query, kode status, penanganan galat, hingga paginasi. Seluruh contoh dijalankan terhadap server HTTP lokal sebagai simulasi API sehingga hasilnya selalu dapat direproduksi.

Apa itu REST API #

REST API adalah API yang mengikuti gaya arsitektur REST: setiap sumber daya (data cuaca, daftar produk, dan sebagainya) dialamatkan lewat sebuah URL yang disebut endpoint. Program kita mengirim permintaan dengan salah satu method HTTP, lalu server membalas dengan data — paling sering dalam format JSON. Sebelum membahas kode, kita bedah dulu anatomi sebuah URL permintaan:

BagianContohPeran
SkemahttpsProtokol komunikasi yang dipakai
Domainapi.example.comAlamat server penyedia API
Endpoint (path)/v1/cuacaSumber daya yang diminta: data cuaca
Query string?kota=BandungSaringan tambahan pada permintaan

Method HTTP menunjukkan niat permintaan: GET mengambil data, POST mengirim data baru, PUT memperbarui data, dan DELETE menghapus data. Untuk keperluan pengumpulan data, kita hampir selalu cukup memakai GET. Format JSON sendiri adalah teks berpasangan kunci-nilai yang bentuknya mirip dictionary Python — dokumentasi modul json menjelaskan pegangan lengkapnya, dan method respons.json() yang kita pakai nanti bekerja di atas modul tersebut.

Permintaan GET Pertama dengan requests #

Kita mulai dengan contoh paling sederhana: meminta data cuaca dari endpoint /api/cuaca. Contoh pada chapter ini menyalakan server HTTP kecil di dalam proses Python yang sama, mendengarkan alamat 127.0.0.1 (komputer sendiri) pada port acak. Pola ini membuat perilaku API sesungguhnya — koneksi, kode status, dan isi JSON — dapat kita amati tanpa bergantung pada layanan luar.

python
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests

class HandlerCuaca(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/api/cuaca":
            body = json.dumps({"kota": "Bandung", "suhu_c": 24, "kelembapan": 78}).encode()
            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)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), HandlerCuaca)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"

respons = requests.get(f"{basis_url}/api/cuaca", timeout=5)
print("Kode status :", respons.status_code)
print("Tipe konten :", respons.headers["Content-Type"])
print("Isi mentah  :", respons.text)

data = respons.json()
print("Kota        :", data["kota"])
print("Suhu (C)    :", data["suhu_c"])

server.shutdown()

Output yang Dihasilkan:

text
Kode status : 200
Tipe konten : application/json
Isi mentah  : {"kota": "Bandung", "suhu_c": 24, "kelembapan": 78}
Kota        : Bandung
Suhu (C)    : 24

Mari kita telaah baris-baris kode di atas:

  1. Baris 6-20 (class HandlerCuaca): sisi server simulasi. Method do_GET menentukan balasan untuk setiap path; untuk /api/cuaca server menuliskan JSON hasil json.dumps(), sedangkan path lain dibalas 404. Method log_message dimatikan supaya server tidak mencetak log ke stderr.
  2. Baris 22-24 (server = ... hingga basis_url = ...): server dinyalakan pada port 0, artinya sistem operasi memilih port bebas — ini menghindari benturan dengan aplikasi lain. Alamat dasar API disimpan ke variabel basis_url.
  3. Baris 26 (respons = requests.get(...)): inti chapter ini. Kita memanggil requests.get() dengan URL endpoint dan timeout=5 — batas waktu 5 detik agar program tidak menggantung menunggu balasan yang tidak pernah datang.
  4. Baris 27-29: respons.status_code berisi kode status, respons.headers berisi metadata balasan, dan respons.text berisi isi mentah balasan sebagai string.
  5. Baris 31-33 (data = respons.json()): isi JSON diuraikan menjadi dictionary Python, sehingga setiap nilainya dapat diakses dengan data["kota"] dan data["suhu_c"].
  6. Baris 35 (server.shutdown()): server dimatikan agar program berakhir dengan bersih.

Parameter Query dan Header Permintaan #

Endpoint yang sama dapat mengembalikan data berbeda tergantung parameter query-nya. Daripada menempelkan ?kota=Bandung secara manual ke URL, kita mengirim dictionary params dan requests akan menyusun query string untuk kita — termasuk mengubah spasi dan karakter khusus menjadi bentuk yang sah untuk URL. Header permintaan dikirim lewat dictionary headers; dua yang sering dipakai adalah User-Agent (identitas program kita) dan Accept (format balasan yang kita harapkan).

python
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import requests

CUACA = {
    "Bandung": {"suhu_c": 24, "kelembapan": 78},
    "Surabaya": {"suhu_c": 32, "kelembapan": 65},
}

class HandlerCuaca(BaseHTTPRequestHandler):
    def do_GET(self):
        hasil = urlparse(self.path)
        if hasil.path == "/api/cuaca":
            kota = parse_qs(hasil.query).get("kota", ["Bandung"])[0]
            data = CUACA.get(kota)
            if data is None:
                body = json.dumps({"pesan": "kota tidak dikenal"}).encode()
                kode = 404
            else:
                body = json.dumps({"kota": kota, **data}).encode()
                kode = 200
            self.send_response(kode)
            self.send_header("Content-Type", "application/json")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            self.wfile.write(body)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), HandlerCuaca)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"

header_kita = {"User-Agent": "TokoKopiKita/1.0", "Accept": "application/json"}
for kota in ["Bandung", "Surabaya"]:
    respons = requests.get(
        f"{basis_url}/api/cuaca",
        params={"kota": kota},
        headers=header_kita,
        timeout=5,
    )
    print("Query pada URL :", urlparse(respons.url).query)
    print("Kode status    :", respons.status_code)
    print("Data           :", respons.json())
    print()

server.shutdown()

Output yang Dihasilkan:

text
Query pada URL : kota=Bandung
Kode status    : 200
Data           : {'kota': 'Bandung', 'suhu_c': 24, 'kelembapan': 78}

Query pada URL : kota=Surabaya
Kode status    : 200
Data           : {'kota': 'Surabaya', 'suhu_c': 32, 'kelembapan': 65}

Poin penting dari blok di atas:

  1. Baris 14-16 (hasil = urlparse(self.path) dst.): di sisi server, urlparse memisahkan path dari query string, lalu parse_qs mengurai query menjadi dictionary. Server simulasi kita membalaskan data sesuai kota yang diminta.
  2. Baris 40-47 (header_kita dan requests.get(...)): header User-Agent mengidentifikasi program kita di mata server, lalu params={"kota": kota} membuat requests menambahkan ?kota=Bandung pada URL secara otomatis — terlihat pada keluaran urlparse(respons.url).query. Pendekatan params lebih aman daripada menggabungkan string manual karena requests menangani pengodean URL.

Kode Status dan Penanganan Galat #

Setiap balasan HTTP membawa kode status tiga digit yang meringkas hasil permintaan. Tiga kode yang paling sering kita jumpai:

KodeNamaArtiTindakan kita
200OKPermintaan berhasilBaca isi balasan
404Not FoundSumber daya tidak adaPeriksa penulisan endpoint dan parameter
500Internal Server ErrorGalat pada sisi serverCoba beberapa saat lagi; laporkan bila berlanjut

Galat saat memakai API terbagi dalam dua keluarga: respons berstatus galat (4xx dan 5xx) yang tetap menghasilkan objek respons, dan kegagalan jaringan (server tidak merespons, koneksi ditolak) yang langsung memunculkan exception sebelum balasan diterima. Method raise_for_status() mengubah keluarga pertama menjadi exception, sehingga keduanya dapat kita tangani dengan satu pola try/except yang sama.

python
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
import requests

class HandlerAneka(BaseHTTPRequestHandler):
    def do_GET(self):
        if self.path == "/api/cuaca":
            body = json.dumps({"kota": "Bandung", "suhu_c": 24}).encode()
            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 == "/api/galat":
            self.send_response(500)
            self.end_headers()
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), HandlerAneka)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"

for path in ["/api/cuaca", "/api/galat", "/api/tidak-ada"]:
    respons = requests.get(f"{basis_url}{path}", timeout=5)
    print(f"{path:18} -> {respons.status_code}")

try:
    respons = requests.get(f"{basis_url}/api/galat", timeout=5)
    respons.raise_for_status()
except requests.RequestException as galat:
    print("Galat HTTP tertangkap :", type(galat).__name__)
    print("Program tetap berjalan karena galat ditangani")

try:
    requests.get("http://127.0.0.1:1/api/cuaca", timeout=5)
except requests.RequestException as galat:
    print("Galat koneksi tertangkap:", type(galat).__name__)

server.shutdown()

Output yang Dihasilkan:

text
/api/cuaca         -> 200
/api/galat         -> 500
/api/tidak-ada     -> 404
Galat HTTP tertangkap : HTTPError
Program tetap berjalan karena galat ditangani
Galat koneksi tertangkap: ConnectionError

Mari kita telaah bagian-bagian kuncinya:

  1. Baris 29-31 (for path in ...): tiga permintaan ke tiga path menghasilkan tiga kode status yang berbeda (200, 500, 404) — semuanya tetap menghasilkan objek respons, hanya isi dan kodenya yang berbeda.
  2. Baris 34-35 (respons.raise_for_status()): karena status 500 tergolong galat, method ini memunculkan requests.HTTPError. Tanpa raise_for_status(), kode 500 akan lolos diam-diam dan analisis kita memroses balasan yang kosong.
  3. Baris 36 (except requests.RequestException as galat): RequestException adalah induk dari seluruh jenis galat requests, sehingga satu blok except cukup untuk menangkap HTTPError, ConnectionError, Timeout, dan lainnya.
  4. Baris 41 (requests.get("http://127.0.0.1:1/...")): port 1 hampir pasti tidak ada layanannya, sehingga permintaan ini gagal di tahap koneksi dan memunculkan ConnectionError — tetap tertangkap oleh except yang sama.

Paginasi: Mengambil Data Per Halaman #

API yang menyimpan banyak data hampir tidak pernah mengirim semuanya sekaligus; data dibagi menjadi halaman (paginasi) agar satu balasan tetap ringan. Pola umumnya: kirim parameter page, terima sepotong data beserta penunjuk halaman berikutnya, ulangi sampai penunjuk tersebut kosong. Simulasi berikut melayani enam produk dalam dua halaman, tiga produk per halaman.

python
import json
import threading
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import pandas as pd
import requests

PRODUK = [
    {"produk": "Kopi Susu", "harga": 24000},
    {"produk": "Americano", "harga": 22000},
    {"produk": "Matcha Latte", "harga": 28000},
    {"produk": "Teh Tawar", "harga": 20000},
    {"produk": "Kopi Susu Dingin", "harga": 26000},
    {"produk": "Biji Kopi 200g", "harga": 85000},
]

class HandlerProduk(BaseHTTPRequestHandler):
    def do_GET(self):
        hasil = urlparse(self.path)
        if hasil.path == "/api/produk":
            halaman = int(parse_qs(hasil.query).get("page", ["1"])[0])
            awal = (halaman - 1) * 3
            potongan = PRODUK[awal:awal + 3]
            berikutnya = halaman + 1 if awal + 3 < len(PRODUK) else None
            body = json.dumps(
                {"halaman": halaman, "halaman_berikutnya": berikutnya, "hasil": potongan}
            ).encode()
            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)
        else:
            self.send_response(404)
            self.end_headers()

    def log_message(self, *args):
        pass

server = HTTPServer(("127.0.0.1", 0), HandlerProduk)
threading.Thread(target=server.serve_forever, daemon=True).start()
basis_url = f"http://127.0.0.1:{server.server_port}"

semua_baris = []
halaman = 1
while True:
    respons = requests.get(
        f"{basis_url}/api/produk", params={"page": halaman}, timeout=5
    )
    data = respons.json()
    semua_baris.extend(data["hasil"])
    print(f"Halaman {halaman}: {len(data['hasil'])} baris, berikutnya: {data['halaman_berikutnya']}")
    if data["halaman_berikutnya"] is None:
        break
    halaman += 1

df = pd.DataFrame(semua_baris)
print()
print(df)

server.shutdown()

Output yang Dihasilkan:

text
Halaman 1: 3 baris, berikutnya: 2
Halaman 2: 3 baris, berikutnya: None

             produk  harga
0         Kopi Susu  24000
1         Americano  22000
2      Matcha Latte  28000
3         Teh Tawar  20000
4  Kopi Susu Dingin  26000
5    Biji Kopi 200g  85000

Alur pengambilan bertahap di atas bekerja seperti ini:

  1. Baris 44-45 (semua_baris = [] dan halaman = 1): wadah hasil disiapkan dan penanda halaman dimulai dari satu.
  2. Baris 46-55 (while True: ...): pada setiap putaran, satu halaman diminta dengan params={"page": halaman}; isi kolom hasil ditambahkan ke semua_baris dengan extend(). Ketika halaman_berikutnya bernilai None, loop berhenti — inilah syarat berhenti yang ditentukan server, bukan ditebak oleh kita.
  3. Baris 57-59 (df = pd.DataFrame(semua_baris)): karena setiap baris hasil berupa dictionary dengan kunci yang sama, pd.DataFrame() langsung membentuk tabel yang rapi — titik temu antara pengambilan data dari API dan analisis pandas yang telah kita kenal di tutorial intro pandas .

Perlu dicatat bahwa penanda halaman berbeda-beda antar API: sebagian memakai nomor halaman berikutnya seperti di atas, sebagian memakai token khusus (cursor), dan sebagian cukup memberi tahu total halaman. Prinsip loop-nya sama: ambil, kumpulkan, periksa penanda, ulangi.

Rangkuman #

  • REST API memaparkan data lewat endpoint; method GET untuk mengambil; balasan umumnya berformat JSON yang dapat diurai dengan respons.json().
  • requests.get(url, params=..., headers=..., timeout=...) adalah pola dasar permintaan; params menangani query string dan headers membawa identitas serta preferensi format.
  • Kode status 200, 404, dan 500 menuntut tindakan berbeda; raise_for_status() bersama try/except requests.RequestException menangani keluarga galat HTTP sekaligus kegagalan koneksi.
  • Paginasi diselesaikan dengan loop halaman yang berhenti mengikuti penanda dari server, lalu seluruh hasil digabung menjadi satu DataFrame.

Sejauh ini data kita datang dari layanan yang memang dirancang untuk dibaca program. Namun di banyak organisasi, data yang paling kaya justru tersimpan di database: tabel penjualan, tabel pelanggan, dan tabel stok yang hidup di sistem operasional. Pada chapter berikutnya kita belajar menyambungkan Python ke database SQL dengan modul sqlite3 dan membaca hasil query langsung ke pandas di chapter Pengambilan Data dari Database.