Server TCP pada chapter sebelumnya menunjukkan inti model serving, tetapi protokol baris-per-baris itu hanya nyaman bagi klien yang kita tulis sendiri. Dunia aplikasi berbicara dalam HTTP: endpoint, status code, JSON, dan validasi terstandar. REST API untuk model membuat layanan prediksi bisa diakses dari bahasa pemrograman mana pun, didokumentasikan otomatis, dan ditolak dengan rapi saat input tidak valid. Chapter ini membangun endpoint POST /predict dengan FastAPI — mulai dari skema input, inference, sampai perilaku galat — lalu mengujinya lewat TestClient tanpa membuka port sungguhan.

Anatomi Endpoint Prediksi #

Sebuah endpoint prediksi minimal memuat empat bagian: skema input yang tervalidasi, konversi JSON ke tensor, pemanggilan model, dan skema respons. FastAPI menjahit keempatnya lewat anotasi tipe: parameter fungsi endpoint bertipe BaseModel otomatis divalidasi Pydantic sebelum fungsi berjalan, dan nilai kembali bertipe BaseModel otomatis diserialisasi ke JSON.

BagianAlatPeran
Skema inputBaseModel + field_validatormenolak JSON yang salah bentuk sebelum menyentuh model
Konversitorch.tensor(...)mengubah daftar angka menjadi tensor batch 1
Inferencemodel(x) dalam no_grad()menghitung probabilitas kelas
Skema responsresponse_model=...menjamin bentuk JSON keluar yang konsisten

Endpoint GET /health melengkapi layanan: jalur ringan tanpa model yang dipakai load balancer dan sistem pemantau untuk memeriksa kehidupan server — pola yang sudah kita lihat pada health check di Model Serving .

Praktik: FastAPI dengan Validasi dan Uji Nyata #

Berkas rest_api_predict.py memuat model terlatih di tingkat modul (sekali saat proses menyala), mendefinisikan dua skema Pydantic, lalu menguji empat skenario: dua prediksi valid, satu input salah panjang, satu input salah tipe, dan satu health check. TestClient menjalankan seluruh tumpukan ASGI di dalam proses yang sama sehingga pengujian tidak membutuhkan server terpisah:

python
"""REST API inferensi dengan FastAPI + TestClient.

Alur: definisi skema Pydantic (validasi input) -> aplikasi FastAPI
dengan endpoint POST /predict dan GET /health -> uji lewat TestClient:
dua prediksi valid, input salah panjang (422), input salah tipe (422),
dan health check.
"""

import torch
import torch.nn as nn
from fastapi import FastAPI
from fastapi.testclient import TestClient
from pydantic import BaseModel, field_validator
from sklearn.datasets import make_moons
from torch.utils.data import DataLoader, TensorDataset

torch.manual_seed(42)
X, y = make_moons(n_samples=256, noise=0.20, random_state=42)
X = (X - X.mean(axis=0)) / X.std(axis=0)
loader = DataLoader(
    TensorDataset(
        torch.tensor(X, dtype=torch.float32),
        torch.tensor(y, dtype=torch.long),
    ),
    batch_size=32,
    shuffle=True,
    num_workers=0,
    generator=torch.Generator().manual_seed(42),
)
model = nn.Sequential(nn.Linear(2, 12), nn.ReLU(), nn.Linear(12, 2))
kriteria = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD(model.parameters(), lr=0.5)
for _ in range(20):
    for X_batch, y_batch in loader:
        optimizer.zero_grad()
        loss = kriteria(model(X_batch), y_batch)
        loss.backward()
        optimizer.step()
model.eval()

app = FastAPI(title="API Klasifikasi Mini", version="1.0.0")


class PermintaanPrediksi(BaseModel):
    """Skema body untuk POST /predict."""

    fitur: list[float]

    @field_validator("fitur")
    @classmethod
    def cek_panjang(cls, nilai):
        if len(nilai) != 2:
            raise ValueError("fitur harus berisi tepat 2 angka")
        return nilai


class HasilPrediksi(BaseModel):
    """Skema respons dari POST /predict."""

    kelas: int
    label: str
    probabilitas: list[float]


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


@app.post("/predict", response_model=HasilPrediksi)
def predict(permintaan: PermintaanPrediksi):
    x = torch.tensor([permintaan.fitur], dtype=torch.float32)
    with torch.no_grad():
        probabilitas = torch.softmax(model(x), dim=1)[0].tolist()
    kelas = probabilitas.index(max(probabilitas))
    return HasilPrediksi(
        kelas=kelas,
        label=f"kelas_{kelas}",
        probabilitas=[round(p, 4) for p in probabilitas],
    )


client = TestClient(app)

daftar_uji = [
    {"fitur": [0.5, -0.8]},
    {"fitur": [-1.2, 0.9]},
    {"fitur": [1.0, 2.0, 3.0]},
    {"fitur": ["nol", "satu"]},
]
for body in daftar_uji:
    respons = client.post("/predict", json=body)
    if respons.status_code == 200:
        print(f"POST /predict {str(body):26s} -> 200 {respons.json()}")
    else:
        pesan = respons.json()["detail"][0]["msg"]
        print(f"POST /predict {str(body):26s} -> {respons.status_code} {pesan}")

respons = client.get("/health")
print(f"GET /health                       -> {respons.status_code} {respons.json()}")

Output yang Dihasilkan:

text
POST /predict {'fitur': [0.5, -0.8]}     -> 200 {'kelas': 1, 'label': 'kelas_1', 'probabilitas': [0.1313, 0.8687]}
POST /predict {'fitur': [-1.2, 0.9]}     -> 200 {'kelas': 0, 'label': 'kelas_0', 'probabilitas': [0.9718, 0.0282]}
POST /predict {'fitur': [1.0, 2.0, 3.0]} -> 422 Value error, fitur harus berisi tepat 2 angka
POST /predict {'fitur': ['nol', 'satu']} -> 422 Input should be a valid number, unable to parse string as a number
GET /health                       -> 200 {'status': 'ok', 'versi': '1.0.0'}

Mari kita telaah kode di atas baris per baris:

  1. Baris 16-37 (model di tingkat modul): training berjalan sekali saat modul diimpor — pola yang di produksi diganti pemuatan paket dari Menyimpan Model untuk Produksi . Endpoint tidak pernah melatih ulang apa pun.
  2. Baris 40 (app = FastAPI(...)): satu objek aplikasi menampung seluruh endpoint, lengkap dengan judul dan versi yang muncul di dokumentasi otomatis /docs.
  3. Baris 43-52 (PermintaanPrediksi): tipe list[float] menolak string dan objek sejak validasi; field_validator menambah aturan panjang tepat dua. Input [1.0, 2.0, 3.0] gagal di validator kustom kita, sedangkan ["nol", "satu"] sudah ditolak Pydantic sebelum sampai ke sana — keduanya pulang sebagai 422 tanpa pernah menyentuh model.
  4. Baris 55-60 (HasilPrediksi): skema respons memaksa bentuk keluaran konsisten: kelas bilangan bulat, label teks, probabilitas daftar angka. Klien dapat menulis kode terhadap kontrak ini tanpa membaca isi server.
  5. Baris 71-80 (endpoint predict): alur lurus — bangun tensor batch 1, inference tanpa gradian, lalu bungkus hasil. Dekorator response_model memvalidasi nilai kembali terhadap HasilPrediksi sebelum dikirim.
  6. Baris 91-99 (loop uji): TestClient mengirim JSON seperti HTTP client sungguhan dan melaporkan status code. Status 422 adalah konvensi FastAPI untuk body yang gagal validasi — klien mendapat pesan galat yang menjelaskan sebabnya, bukan kegagalan internal 500.

Menjalankan server sungguhan untuk pengembangan lokal cukup satu perintah setelah menyimpan bagian aplikasi ke berkas terpisah — misalnya api.py dengan objek app:

bash
uvicorn api:app --host 127.0.0.1 --port 8000

Dokumentasi interaktif Swagger UI tersedia otomatis di http://127.0.0.1:8000/docs, dan pola request body serta pengujian dengan TestClient dibahas resmi di fastapi.tiangolo.com/tutorial/body dan fastapi.tiangolo.com/tutorial/testing .

Endpoint per-permintaan seperti ini menuntut satu hal yang belum kita ukur: waktu. Berapa lama satu prediksi benar-benar berjalan, dan berapa banyak yang bisa dilayani per detik? Dua pertanyaan itu membawa kita ke pemisahan antara batch dan real-time inference pada dua chapter berikutnya.