Bab 21 — Bekerja dengan API: Meminta Data dari Komputer Lain

Seri I2 — Data dan Internet

CatatanDi bab ini, Anda akan:
  • Memahami apa itu API, HTTP, dan JSON dengan analogi pelayan restoran
  • Mengirim permintaan dengan requests dan httpx, lalu membaca jawabannya
  • Memakai parameter dan token (kunci masuk) tanpa menulis rahasia di kode
  • Menangani galat jaringan: kode status, timeout, dan coba ulang
  • Mengambil data cuaca dan harga untuk Proyek Bersambung I2

Semua skrip bab ini ada di folder kode/bab21/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).

Seri I1 membuat kode Anda rapi. Sekarang kode itu perlu bahan, dan banyak bahan ada di komputer lain: prakiraan cuaca di server badan cuaca, harga di server pasar. Bayangkan sebuah restoran. Anda (program) duduk di meja, pelayan (API) menerima pesanan, dan dapur (server) memasak. Anda tidak perlu masuk dapur; cukup memesan dengan cara yang sudah ditentukan, lalu menerima piring berisi jawabannya. API (Application Programming Interface) adalah “daftar menu dan aturan memesan” itu.

Satu percakapan dengan API: Anda mengirim permintaan, server membalas dengan kode status dan isi JSON.

HTTP dan JSON dalam lima menit

Percakapan itu memakai bahasa bernama HTTP. Sebuah permintaan berisi: metode (GET = “tolong berikan”), alamat, dan kadang tambahan seperti parameter atau token. Sebuah jawaban berisi: kode status (angka tiga digit), header (keterangan), dan isi. Isi dari API biasanya berbentuk JSON, yaitu teks yang susunannya mirip dict dan list Python (Bab 9).

Kode Artinya Tindakan Anda
200 Berhasil Baca isinya
400 / 404 Permintaan keliru / alamat tidak ada Periksa alamat dan parameter; mengulang tidak membantu
401 Perlu token yang benar Periksa token
500-an (mis. 503) Server bermasalah atau sibuk Boleh coba ulang nanti

Server tiruan: dapur milik kita sendiri

Agar contoh buku ini selalu menghasilkan keluaran yang sama dan tidak membebani server orang lain, kita membuat server API tiruan di komputer sendiri. File pembantunya server_tiruan.py (ada di folder bab, bukan skrip contoh) memakai http.server bawaan Python. Ia menyala di alamat lokal 127.0.0.1 pada port acak, melayani beberapa alamat dengan data fiktif, lalu dimatikan saat blok with selesai. Bagi requests, ia tampak seperti API sungguhan.

Permintaan pertama dengan requests

requests adalah pustaka pihak ketiga yang paling dikenal untuk HTTP (sudah ada di environment proyek ini; di komputer Anda: pip install requests).

01_permintaan_dasar.py
# Bab 21 - Permintaan HTTP pertama dengan requests
import requests
from server_tiruan import ServerTiruan

with ServerTiruan() as server:          # menyalakan server tiruan di komputer sendiri
    jawaban = requests.get(f"{server.alamat}/cuaca?lokasi=bogor&hari=2", timeout=5)

    print("Kode status :", jawaban.status_code)
    print("Jenis isi   :", jawaban.headers["Content-Type"])
    print("Teks mentah :", jawaban.text)
# keluar dari blok `with`: server dimatikan lagi
Kode status : 200
Jenis isi   : application/json; charset=utf-8
Teks mentah : {"lokasi": "bogor", "satuan": {"suhu_maks": "C", "hujan_mm": "mm"}, "harian": {"tanggal": ["2026-01-01", "2026-01-02"], "suhu_maks": [30.1, 31.4], "hujan_mm": [12.5, 0.0]}}

Kode 200 berarti berhasil. Content-Type memberi tahu bahwa isinya JSON. “Teks mentah” itu dikirim server sebagai satu baris panjang; berikutnya kita ubah menjadi data Python.

JSON dan parameter

02_json_dan_parameter.py
# Bab 21 - JSON dan parameter (params)
import json
import requests
from server_tiruan import ServerTiruan

with ServerTiruan() as server:
    # params= menyusun bagian "?kunci=nilai" dengan benar (termasuk penyandian karakter)
    jawaban = requests.get(f"{server.alamat}/cuaca", params={"lokasi": "bandung", "hari": 3}, timeout=5)
    print("Alamat lengkap:", jawaban.url.replace(server.alamat, "<server>"))

    data = jawaban.json()               # JSON -> dict/list Python
    print("Tipe data    :", type(data).__name__)
    print("Lokasi       :", data["lokasi"])
    print("Tanggal      :", data["harian"]["tanggal"])
    print("Hujan (mm)   :", data["harian"]["hujan_mm"])
    print("Total hujan  :", round(sum(data["harian"]["hujan_mm"]), 1), "mm")

    # arah sebaliknya: Python -> teks JSON
    print(json.dumps({"lokasi": "bandung", "hujan_total_mm": 19.2}, ensure_ascii=False, indent=2))
Alamat lengkap: <server>/cuaca?lokasi=bandung&hari=3
Tipe data    : dict
Lokasi       : bandung
Tanggal      : ['2026-01-01', '2026-01-02', '2026-01-03']
Hujan (mm)   : [5.0, 0.0, 14.2]
Total hujan  : 19.2 mm
{
  "lokasi": "bandung",
  "hujan_total_mm": 19.2
}
  • params={...} menyusun bagian ?lokasi=bandung&hari=3 dengan benar. Jangan merangkai alamat dengan tangan.
  • .json() mengubah jawaban menjadi dict/list. Tiga hari hujan Bandung: 5,0 + 0,0 + 14,2 = 19,2 mm.
  • json.dumps(...) arah sebaliknya: dari Python menjadi teks JSON (indent=2 merapikan).

httpx: saudara requests

httpx mirip requests tetapi punya Client: objek yang mengingat alamat dasar dan pengaturan sehingga tidak diulang di tiap panggilan (juga mendukung mode asinkron yang dibahas di seri lanjut).

03_httpx.py
# Bab 21 - httpx: saudara requests, dengan Client yang bisa dipakai ulang
import httpx
from server_tiruan import ServerTiruan

with ServerTiruan() as server:
    # Client menyimpan alamat dasar dan pengaturan, jadi tidak diulang di tiap panggilan
    with httpx.Client(base_url=server.alamat, timeout=5.0) as klien:
        for lokasi in ["bogor", "bandung", "malang"]:
            jawaban = klien.get("/cuaca", params={"lokasi": lokasi, "hari": 5})
            data = jawaban.json()
            hujan = data["harian"]["hujan_mm"]
            print(f"{lokasi:8} status {jawaban.status_code}  total hujan {sum(hujan):5.1f} mm")
bogor    status 200  total hujan  49.2 mm
bandung  status 200  total hujan  51.5 mm
malang   status 200  total hujan  28.7 mm

Tiga lokasi, total hujan lima hari masing-masing 49,2; 51,5; dan 28,7 mm (data fiktif). Untuk pemula, pilih salah satu dulu; ilmunya hampir sama.

Autentikasi: menunjukkan “kartu anggota”

Banyak API hanya melayani pemegang token (semacam kartu anggota). Cara paling umum: mengirim token di header Authorization: Bearer <token>. Cara lain: API key di header atau parameter, atau Basic auth (nama pengguna dan kata sandi; di requests lewat auth=(nama, sandi)).

04_autentikasi.py
# Bab 21 - Autentikasi dasar: token di header Authorization
import os
import requests
from server_tiruan import ServerTiruan

with ServerTiruan() as server:
    alamat = f"{server.alamat}/rahasia"

    tanpa = requests.get(alamat, timeout=5)
    print("Tanpa token :", tanpa.status_code, tanpa.json())

    # Di dunia nyata, token dibaca dari variabel lingkungan, TIDAK ditulis di kode.
    # Di sini token acak milik server tiruan kita titipkan ke variabel lingkungan sementara.
    os.environ["TOKEN_API_CONTOH"] = server.token
    token = os.environ["TOKEN_API_CONTOH"]

    dengan = requests.get(alamat, headers={"Authorization": f"Bearer {token}"}, timeout=5)
    print("Dengan token:", dengan.status_code, dengan.json())
Tanpa token : 401 {'galat': 'token tidak ada atau salah'}
Dengan token: 200 {'pesan': 'Anda sudah masuk', 'petugas': 'fiktif'}

Tanpa token server menjawab 401; dengan token menjawab 200. Perhatikan cara token diperlakukan: dibaca dari variabel lingkungan (os.environ), tidak ditulis di kode. Di sini token acak dibuat server tiruan, jadi tidak ada rahasia sungguhan.

PeringatanAWAS

Jangan pernah menulis token, API key, atau kata sandi langsung di kode, apalagi bila kode itu akan diunggah ke GitHub (Bab 20). Simpan di variabel lingkungan atau berkas .env yang masuk .gitignore.

Ketika keadaan tidak mulus

Jaringan itu tidak pasti: alamat salah, server mati, koneksi putus. Program yang baik mengantisipasinya.

Kode status dan raise_for_status()

05_galat_status.py
# Bab 21 - Kode status galat dan raise_for_status()
import requests
from server_tiruan import ServerTiruan

with ServerTiruan() as server:
    for alamat in ["/cuaca?lokasi=bogor&hari=1", "/cuaca?lokasi=atlantis", "/cuaca", "/rahasia"]:
        jawaban = requests.get(server.alamat + alamat, timeout=5)
        print(f"{alamat:28} -> {jawaban.status_code}")

    # raise_for_status() mengubah kode 4xx/5xx menjadi pengecualian yang bisa ditangkap
    try:
        requests.get(f"{server.alamat}/cuaca?lokasi=atlantis", timeout=5).raise_for_status()
    except requests.HTTPError as galat:
        print("Tertangkap HTTPError:", galat.response.status_code, galat.response.json())
/cuaca?lokasi=bogor&hari=1   -> 200
/cuaca?lokasi=atlantis       -> 404
/cuaca                       -> 400
/rahasia                     -> 401
Tertangkap HTTPError: 404 {'galat': 'lokasi tidak dikenal'}

requests tidak menganggap 404 sebagai galat; itu hanya jawaban dengan kode berbeda. raise_for_status() mengubah kode 4xx/5xx menjadi pengecualian HTTPError yang bisa Anda try/except (Bab 12).

Timeout: jangan menunggu selamanya

06_timeout.py
# Bab 21 - Timeout: jangan menunggu selamanya
import requests
from server_tiruan import ServerTiruan

with ServerTiruan() as server:      # alamat /lambat sengaja membalas setelah 1,5 detik
    try:
        requests.get(f"{server.alamat}/lambat", timeout=0.3)
    except requests.Timeout:
        print("Server terlalu lama menjawab (> 0,3 detik): permintaan dibatalkan.")

    jawaban = requests.get(f"{server.alamat}/lambat", timeout=5)
    print("Dengan batas 5 detik:", jawaban.json()["pesan"])

# Tanpa timeout=, requests menunggu tanpa batas waktu (lihat dokumentasi resmi).
Server terlalu lama menjawab (> 0,3 detik): permintaan dibatalkan.
Dengan batas 5 detik: akhirnya selesai
CatatanINGAT

Menurut dokumentasi resmi requests, bila timeout= tidak diberikan, permintaan tidak punya batas waktu dan program bisa menggantung. Biasakan selalu mengisinya. Di httpx, batas bawaannya 5 detik.

Coba ulang dengan jeda

Galat 5xx sering sementara. Strateginya: coba lagi dengan jeda yang makin panjang (exponential backoff), tetapi hanya untuk galat yang memang layak diulang, dan berhenti setelah beberapa kali.

%%{init: {"flowchart": {"diagramPadding": 40}}}%%
flowchart TD
  A(["Kirim permintaan"]) --> B{"Status?"}
  B -->|"2xx / 4xx"| C(["Selesai: pakai atau laporkan"])
  B -->|"5xx atau jaringan putus"| D{"Percobaan habis?"}
  D -->|"belum"| E["Tunggu, lalu gandakan jeda"]
  E --> A
  D -->|"sudah"| F(["Menyerah dengan pesan jelas"])

Alur coba ulang: hanya galat sementara (5xx, jaringan) yang diulang, dengan jeda berlipat.

07_coba_ulang.py
# Bab 21 - Coba ulang dengan jeda yang makin panjang
import time
import requests
from server_tiruan import ServerTiruan


def ambil_dengan_ulang(alamat, maks_coba=4, jeda_awal=0.05):
    jeda = jeda_awal
    for percobaan in range(1, maks_coba + 1):
        try:
            jawaban = requests.get(alamat, timeout=5)
            if jawaban.status_code < 500:        # 2xx/4xx: tidak ada gunanya mengulang
                return jawaban
            print(f"  percobaan {percobaan}: status {jawaban.status_code}, tunggu {jeda:.2f} detik")
        except (requests.ConnectionError, requests.Timeout):
            print(f"  percobaan {percobaan}: jaringan bermasalah, tunggu {jeda:.2f} detik")
        time.sleep(jeda)
        jeda = jeda * 2                          # jeda berlipat: 0,05 -> 0,10 -> 0,20 ...
    raise RuntimeError("menyerah setelah beberapa kali mencoba")


with ServerTiruan() as server:      # /sibuk gagal dua kali (503), baru berhasil
    hasil = ambil_dengan_ulang(f"{server.alamat}/sibuk")
    print("Berhasil:", hasil.json())
  percobaan 1: status 503, tunggu 0.05 detik
  percobaan 2: status 503, tunggu 0.10 detik
Berhasil: {'pesan': 'berhasil', 'percobaan_ke': 3}

Server tiruan sengaja gagal dua kali (503), jadi percobaan ke-3 berhasil. Jeda berlipat dua tiap kali: 0,05 lalu 0,10 detik (di dunia nyata awali dengan 1 detik atau lebih agar tidak menghujani server).

Satu panggilan ke API nyata: Open-Meteo

Open-Meteo menyediakan prakiraan cuaca lewat API terbuka. Hasil pemeriksaan syaratnya pada 2026-10-10 (open-meteo.com/en/terms):

  • Penggunaan gratis hanya untuk non-komersial dan tanpa kunci; batasnya kurang dari 10.000 panggilan per hari, 5.000 per jam, dan 600 per menit.
  • Memakai API gratis berarti menerima lisensi CC BY 4.0, yaitu wajib mencantumkan atribusi. Itu sebabnya skrip mencetak “Data cuaca oleh Open-Meteo.com”.
  • Situs ini bukan aplikasi komersial; pembaca yang membuat produk berbayar harus membaca syarat itu sendiri.
08_api_nyata.py
# Bab 21 - Satu panggilan ke API nyata: Open-Meteo (tanpa kunci)
import requests

alamat = "https://api.open-meteo.com/v1/forecast"
parameter = {
    "latitude": -6.6,                  # sekitar Bogor
    "longitude": 106.8,
    "daily": "temperature_2m_max,precipitation_sum",
    "timezone": "Asia/Jakarta",
    "forecast_days": 3,
}
jawaban = requests.get(alamat, params=parameter, timeout=10)
jawaban.raise_for_status()
data = jawaban.json()

print("Status        :", jawaban.status_code)
print("Satuan        :", data["daily_units"])
for tgl, suhu, hujan in zip(data["daily"]["time"], data["daily"]["temperature_2m_max"],
                            data["daily"]["precipitation_sum"]):
    print(f"{tgl}  suhu maks {suhu} C  hujan {hujan} mm")
print("Data cuaca oleh Open-Meteo.com (lisensi CC BY 4.0)")
Status        : 200
Satuan        : {'time': 'iso8601', 'temperature_2m_max': '°C', 'precipitation_sum': 'mm'}
2026-10-10  suhu maks 33.8 C  hujan 1.0 mm
2026-10-11  suhu maks 32.9 C  hujan 5.2 mm
2026-10-12  suhu maks 32.4 C  hujan 6.6 mm
Data cuaca oleh Open-Meteo.com (lisensi CC BY 4.0)

Kode ini tidak dijalankan ulang saat buku dibuat karena butuh internet dan hasilnya berubah setiap hari. Keluaran di atas diambil sendiri pada 2026-10-10 lalu disimpan. Bila Anda menjalankannya, tanggal dan angkanya akan berbeda; itu wajar.

TipTIPS

Soal harga komoditas: pada 2026-10-10 saya tidak menemukan API harga komoditas pertanian yang terverifikasi gratis dan tanpa kunci. Daftar 71 sumber data API World Bank tidak memuat sumber harga komoditas, dan layanan harga komoditas yang muncul di pencarian umumnya meminta pendaftaran atau kunci (syaratnya tidak saya uji). Karena itu harga di buku ini berasal dari server tiruan (fiktif). Untuk proyek sungguhan, periksa sumber resmi seperti kementerian atau bursa komoditas, dan baca syarat penggunaannya.

Proyek Bersambung I2: mengambil data cuaca dan harga

Langkah 1 proyek Seri I2: mengambil cuaca tiga lokasi dan harga empat komoditas dari “API” kita, dengan fungsi kecil yang berhenti jelas bila ada galat, lalu menyimpannya sebagai JSON mentah di folder _tmp.

09_proyek_ambil_data.py
# Bab 21 - Proyek bersambung I2, langkah 1: mengambil data cuaca dan harga
import json
from pathlib import Path

import requests
from server_tiruan import ServerTiruan

tmp = Path(__file__).parent / "_tmp"
tmp.mkdir(exist_ok=True)


def ambil_json(alamat, **param):
    """Mengambil JSON; berhenti dengan pesan jelas bila ada galat."""
    jawaban = requests.get(alamat, params=param, timeout=5)
    jawaban.raise_for_status()
    return jawaban.json()


with ServerTiruan() as server:
    cuaca = {}
    for lokasi in ["bogor", "bandung", "malang"]:
        cuaca[lokasi] = ambil_json(f"{server.alamat}/cuaca", lokasi=lokasi)["harian"]
    harga = ambil_json(f"{server.alamat}/harga")["data"]

(tmp / "cuaca_mentah.json").write_text(json.dumps(cuaca, ensure_ascii=False, indent=1), encoding="utf-8")
(tmp / "harga_mentah.json").write_text(json.dumps(harga, ensure_ascii=False, indent=1), encoding="utf-8")

print("Lokasi cuaca  :", list(cuaca))
print("Hari per lokasi:", len(cuaca["bogor"]["tanggal"]))
print("Baris harga   :", len(harga), "| contoh:", harga[0])
print("Tersimpan di folder _tmp: cuaca_mentah.json, harga_mentah.json")
Lokasi cuaca  : ['bogor', 'bandung', 'malang']
Hari per lokasi: 5
Baris harga   : 20 | contoh: {'tanggal': '2026-01-01', 'komoditas': 'padi', 'harga_per_kg': 6200}
Tersimpan di folder _tmp: cuaca_mentah.json, harga_mentah.json

Cuaca tiga lokasi, lima hari tiap lokasi, dan 20 baris harga (4 komoditas x 5 hari). Data mentah ini akan dibersihkan (Bab 22), digabung (Bab 23), disimpan (Bab 24), dan digambar (Bab 25). Agar tiap skrip berdiri sendiri, bab-bab berikutnya menyalin data yang sama langsung di kodenya.

Latihan

  1. Ubah 02_json_dan_parameter.py agar mengambil malang dan mencetak hari dengan hujan terbanyak.
  2. Di 05_galat_status.py, tambahkan alamat /tidak-ada. Kode status apa yang muncul?
  3. Ubah timeout di 06_timeout.py menjadi 2 detik. Apa yang berubah, dan mengapa?
  4. Tantangan: tulis fungsi ambil_aman(alamat) yang memakai coba ulang untuk 5xx, tetapi langsung berhenti (tanpa mengulang) pada 404, dan mengembalikan None bila akhirnya gagal.

Ringkasan Bab

  • API = cara resmi meminta data dari program lain; HTTP = bahasanya; JSON = bentuk isinya.
  • requests.get(alamat, params=..., timeout=...); .json() mengubah jawaban menjadi data Python. httpx.Client sejenis dengan pengaturan bersama.
  • Kode status: 2xx berhasil, 4xx salah di pihak kita, 5xx salah di server. raise_for_status() mengubah galat menjadi pengecualian.
  • Selalu pakai timeout; coba ulang hanya galat sementara, dengan jeda berlipat.
  • Token dibaca dari variabel lingkungan, bukan ditulis di kode.
  • Open-Meteo: gratis non-komersial, tanpa kunci, wajib atribusi (CC BY 4.0). Harga komoditas gratis tanpa kunci: tidak ditemukan (belum terverifikasi).

Selanjutnya, di Bab 22: data yang datang tidak selalu rapi, misalnya catatan lapangan yang diketik manusia. Kita belajar regex untuk menggali dan membersihkan teks.