Chapter sebelumnya menyimpulkan bahwa mode API real-time adalah pintu masuk serving yang paling instruktif untuk dipraktikkan. Kini kita bangun API tersebut sungguhan: satu layanan HTTP yang menerima fitur rumah, memvalidasi nilainya, mengembalikan prediksi harga, dan melaporkan status kesehatannya. FastAPI dipilih karena tiga hal yang pas untuk machine learning — validasi masukan otomatis lewat Pydantic, skema respons yang eksplisit, dan TestClient yang memungkinkan seluruh API diuji in-process tanpa server nyata. Syarat terakhir tersebut penting untuk lingkungan belajar: skrip di chapter ini berjalan sebagai program Python biasa, deterministik, dan selesai dalam hitungan detik, tetapi polanya identik dengan deployment produksi.

Dua Proses yang Dipisah Artefak #

Arsitektur serving yang sehat memisahkan proses pelatihan dari proses penyajian. Proses pelatihan membaca data, menjalankan fit(), lalu menuliskan hasilnya sebagai artefak — dalam praktik kita, berkas joblib yang memuat Pipeline scikit-learn secara utuh bersama parameter terlatihnya. Proses penyajian kemudian memuat artefak tersebut dan hanya menjalankan predict(); proses penyajian tidak pernah menyentuh data pelatihan.

Pemisahan ini membawa tiga manfaat konkret. Pertama, latensi: joblib.load() dibayar satu kali saat proses menyala, bukan pada setiap permintaan. Kedua, audit: artefak yang sama dapat diuji ulang di mesin lain dan hasilnya dibandingkan. Ketiga, keamanan operasional: proses penyajian tidak membutuhkan akses ke data pelatihan, sehingga lingkup aksesnya bisa dipersempit. Konsep penyimpanan dan pemuatan model dibahas sebagai chapter tersendiri di Section Pipeline dan Produktivitas; di sini kita pakai bentuk paling ringkasnya — joblib.dump() dan joblib.load().

Skema Permintaan dan Validasi Pydantic #

Endpoint prediksi adalah gerbang antara dunia luar yang berisik dan model yang rapuh. Pydantic v2 memungkinkan kita menyatakan kontrak gerbang tersebut sebagai class:

python
class FiturRumah(BaseModel):
    luas: float = Field(ge=20, le=500, description="luas bangunan dalam meter persegi")
    kamar: int = Field(ge=1, le=10, description="jumlah kamar tidur")

Deklarasi luas: float = Field(ge=20, le=500) berarti tiga validasi terjadi sebelum handler berjalan: nilai harus bisa dibaca sebagai float, tidak boleh di bawah 20, dan tidak boleh di atas 500. Permintaan yang melanggar salah satu aturan ditolak otomatis dengan status 422 beserta pesan galat yang menyebutkan field dan alasannya — kita tidak menulis satu baris logika validasi pun di dalam handler. Skema respons (PrediksiHarga) bekerja ke arah sebaliknya: skema tersebut menjamin keluaran endpoint selalu berbentuk yang dijanjikan, termasuk versi_model yang mempercepat penelusuran saat ada laporan dari pengguna.

Menyatukan Semuanya: Skrip Serving Lengkap #

Skrip berikut melakukan lima tahap dalam satu proses: membuat data sintetis, melatih pipeline, menyimpan artefak, memuat artefak kembali ke proses serving, lalu menguji API lewat TestClient. Tidak ada server nyata, tidak ada port yang dibuka — TestClient memanggil handler secara langsung di dalam memori.

python
# Model serving dengan FastAPI: satu proses Python, tanpa server nyata.
# Alur: latih pipeline -> simpan artefak joblib -> muat kembali -> sajikan
# lewat app FastAPI -> uji sepenuhnya in-process dengan TestClient.

import json
import os

import joblib
import numpy as np
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
from sklearn.linear_model import LinearRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler

FOLDER = os.path.dirname(os.path.abspath(__file__))
NAMA_ARTEFAK = "model-rumah.joblib"
JALUR_ARTEFAK = os.path.join(FOLDER, NAMA_ARTEFAK)
VERSI_MODEL = "rumah-v1.0.0"

# 1) Data sintetis: harga rumah (juta rupiah) dari luas dan jumlah kamar.
rng = np.random.default_rng(42)
n_latih = 300
luas = rng.uniform(20.0, 200.0, n_latih)
kamar = rng.integers(1, 7, n_latih)
harga = 3.0 * luas + 10.0 * kamar + rng.normal(0.0, 4.0, n_latih)
X_latih = np.column_stack([luas, kamar])
y_latih = harga

# 2) Latih pipeline lengkap (skala + regresi) lalu simpan sebagai artefak.
pipeline_latih = Pipeline([
    ("skala", StandardScaler()),
    ("regresi", LinearRegression()),
])
pipeline_latih.fit(X_latih, y_latih)
joblib.dump(pipeline_latih, JALUR_ARTEFAK)

# 3) Muat artefak kembali: proses serving tidak melatih apa pun.
model = joblib.load(JALUR_ARTEFAK)

app = FastAPI(title="API Prediksi Harga Rumah", version=VERSI_MODEL)


class FiturRumah(BaseModel):
    luas: float = Field(ge=20, le=500, description="luas bangunan dalam meter persegi")
    kamar: int = Field(ge=1, le=10, description="jumlah kamar tidur")


class PrediksiHarga(BaseModel):
    harga_juta: float
    versi_model: str


@app.get("/health")
def health():
    return {"status": "ok", "versi_model": VERSI_MODEL}


@app.post("/predict")
def predict(fitur: FiturRumah):
    X_baru = np.array([[fitur.luas, fitur.kamar]])
    harga_prediksi = model.predict(X_baru)
    return PrediksiHarga(harga_juta=round(float(harga_prediksi[0]), 3),
                         versi_model=VERSI_MODEL)


# 4) Uji in-process: TestClient menjalankan app tanpa jaringan nyata.
client = TestClient(app)


def uji(label, metode, jalur, **kwargs):
    respons = client.request(metode, jalur, **kwargs)
    print(f"{label}")
    print(f"  status : {respons.status_code}")
    if respons.status_code == 200:
        print(f"  respons: {json.dumps(respons.json(), sort_keys=True)}")
    else:
        for galat in respons.json()["detail"]:
            print(f"  galat  : {galat['type']} pada "
                  f"{'.'.join(str(x) for x in galat['loc'])}: {galat['msg']}")
    print()


uji("GET  /health", "GET", "/health")
uji("POST /predict (valid: luas=120, kamar=3)", "POST", "/predict",
    json={"luas": 120, "kamar": 3})
uji("POST /predict (batas bawah: luas=20, kamar=1)", "POST", "/predict",
    json={"luas": 20, "kamar": 1})
uji("POST /predict (luas di bawah batas: -5)", "POST", "/predict",
    json={"luas": -5, "kamar": 3})
uji("POST /predict (tipe salah: kamar='tiga')", "POST", "/predict",
    json={"luas": 120, "kamar": "tiga"})
uji("POST /predict (field hilang: kamar)", "POST", "/predict",
    json={"luas": 120})

# 5) Bersihkan artefak sementara agar folder chapter tetap rapi.
os.remove(JALUR_ARTEFAK)
print(f"artefak {NAMA_ARTEFAK} dihapus; proses selesai")

Output yang Dihasilkan:

text
GET  /health
  status : 200
  respons: {"status": "ok", "versi_model": "rumah-v1.0.0"}

POST /predict (valid: luas=120, kamar=3)
  status : 200
  respons: {"harga_juta": 389.75, "versi_model": "rumah-v1.0.0"}

POST /predict (batas bawah: luas=20, kamar=1)
  status : 200
  respons: {"harga_juta": 69.505, "versi_model": "rumah-v1.0.0"}

POST /predict (luas di bawah batas: -5)
  status : 422
  galat  : greater_than_equal pada body.luas: Input should be greater than or equal to 20

POST /predict (tipe salah: kamar='tiga')
  status : 422
  galat  : int_parsing pada body.kamar: Input should be a valid integer, unable to parse string as an integer

POST /predict (field hilang: kamar)
  status : 422
  galat  : missing pada body.kamar: Field required

artefak model-rumah.joblib dihapus; proses selesai

Mari kita telaah bagian-bagian yang memikul beban utama:

  1. Baris 17–20 (konstanta): JALUR_ARTEFAK dibangun dari __file__, bukan dari direktori kerja, sehingga skrip menghasilkan artefak di folder yang sama di mana pun skrip dijalankan. VERSI_MODEL akan ikut dalam setiap respons dan endpoint kesehatan.
  2. Baris 23–29 (data sintetis): rng = np.random.default_rng(42) menjamin data pelatihan — dan akibatnya seluruh prediksi — dapat direproduksi bit demi bit.
  3. Baris 32–37 (latih dan simpan): Pipeline berisi StandardScaler dan LinearRegression; seluruh objek terlatih, termasuk rata-rata dan simpangan hasil fit(), tersimpan ke satu berkas oleh joblib.dump().
  4. Baris 40 (model = joblib.load): baris ini adalah batas dua dunia. Setelahnya, variabel model adalah salinan dari artefak; kode di bawahnya tidak lagi bergantung pada proses pelatihan sama sekali.
  5. Baris 45–52 (dua skema): FiturRumah memvalidasi masukan; PrediksiHarga menjanjikan bentuk keluaran. Kedua class tersebut juga otomatis menjadi dokumentasi interaktif di /docs saat API dijalankan dengan server nyata.
  6. Baris 55–57 dan 60–65 (dua endpoint): /health untuk pemeriksaan kesiapan oleh sistem orkestrasi; /predict menerima satu FiturRumah, membentuk array satu baris, lalu memanggil model.predict(). Nilai prediksi dibulatkan tiga desimal supaya respons stabil dan ringkas.
  7. Baris 69 (client = TestClient(app)): dari titik ini, permintaan HTTP disimulasikan di dalam memori. client.post("/predict", json=...) menjalankan seluruh tumpukan FastAPI — routing, validasi Pydantic, serialisasi respons — tanpa membuka port.
  8. Baris 72–95 (helper uji dan enam skenario): helper mencetak status, lalu untuk respons 200 mencetak JSON dengan kunci terurut (sort_keys=True) agar keluaran deterministik, sedangkan untuk 422 mencetak type, loc, dan msg setiap galat validasi. Enam skenario menutup jalur bahagia, nilai batas, pelanggaran rentang, tipe salah, dan field hilang.
  9. Baris 98–99 (pembersihan): artefak sementara dihapus agar folder chapter tetap bersih — kebiasaan yang sama yang kita terapkan pada berkas gambar matplotlib di chapter-chapter sebelumnya.

Perhatikan pola yang berulang pada keluaran: seluruh permintaan tidak valid ditolak dengan status 422 sebelum mencapai handler, dan setiap pesan galat menyebutkan field yang bermasalah. Prediksi luas=120, kamar=3 menghasilkan 389,75 — masuk akal terhadap rumus data sintetis (3,0 × 120 + 10,0 × 3 ≈ 390) — dan nilai batas luas=20 diterima karena ge bersifat inklusif.

Membaca Status Code dan Galat Validasi #

Kontrak status code harus disiplin karena sistem pemantau dan pemanggil API bergantung padanya:

StatusArti bagi pemanggilPenyebab umum
200 OKPrediksi sah dan bisa dipakaiSemua validasi lolos, handler selesai
422 Unprocessable EntityMasukan ditolak sebelum diprosesPelanggaran Field, tipe salah, field hilang
500 Internal Server ErrorSisi layanan bermasalah; coba lagi nantiGalat tak tertangani di dalam handler

Bagi API prediksi, pembeda 422 dan 500 penting: 422 berarti pemanggil yang harus memperbaiki muatannya, sedangkan 500 berarti pemilik layanan yang harus turun tangan. Seluruh kasus pada skenario uji kita jatuh ke kategori pertama, dan itu pertanda kontrak validasi bekerja.

Praktik Operasional di Sekitar Endpoint #

Ketika API ini dipindahkan ke lingkungan nyata, pola yang berubah hanya lapisan terluarnya — handler, skema, dan artefak tetap sama:

bash
uvicorn api-prediksi-rumah:app --host 0.0.0.0 --port 8000

Perintah tersebut menjalankan server ASGI yang melayani app dari berkas api-prediksi-rumah.py; setelahnya dokumentasi interaktif tersedia di /docs. Tiga kebiasaan operasional layak dibawa sejak awal. Pertama, muat artefak sekali di level modul atau startup event, bukan di dalam handler. Kedua, catat setiap permintaan dan prediksi ke penyimpanan log — data tersebut menjadi bahan monitoring pada chapter berikutnya. Ketiga, sisertakan versi_model pada respons seperti pada skrip kita; ketika dua laporan pengguna menunjukkan versi berbeda, penelusuran akar masalah memendek drastis.

Dengan API yang melayani satu permintaan satu prediksi, kita sudah menutup kebutuhan interaktif. Namun tidak semua masalah membutuhkan jawaban seketika: jutaan baris skor kredit semalam tidak memerlukan server yang menyala sepanjang waktu. Pola yang jauh lebih murah untuk kebutuhan tersebut adalah pemrosesan massal terjadwal — materi Chapter Batch Inference.

Referensi Resmi #