Pendahuluan — stub sudah ada, kontraknya belum

Di Class Flask / FastAPI (#52) kamu sudah punya HttpResponse, handle_list, dan handle_create yang runnable tanpa server. Seri 4 Web Lanjut memulai dari sini: memahami kontrak HTTP/REST supaya saat Flask dipasang di artikel berikutnya, route tidak jadi tebak-tebakan.

Artikel ini sengaja tetap di PC tanpa wajib pip install flask. Kita perluas stub jadi “mini router”: method + path + status — lalu baru siap masuk pintu framework.

Prasyarat: OOP Flask / FastAPI (#52) · idealnya Capstone (#49) · Composition (#47) · OOP (#40).

Kenapa HTTP dulu, bukan langsung Flask?

Tanpa kontrak Gejala Dengan kontrak
Langsung tulis @app.get Status “asal 200”; GET mengubah data Method & status dipilih sadar
JSON “asal return” Client bingung sukses vs gagal Body + status = satu paket (HttpResponse)

REST di sini dipakai ringan: resource punya URL, aksi memakai method HTTP — bukan dogma “pure REST” penuh. Nama path mengikuti benda (/api/buku), bukan kata kerja acak seperti /buatBuku — supaya client bisa menebak pola tanpa membaca source.

# Inti yang sudah ada di #52 — kita pakai lagi
class HttpResponse:
    def __init__(self, status, body):
        self.status = status
        self.body = body


ok = HttpResponse(201, {"judul": "OOP Python"})
bad = HttpResponse(400, {"error": "judul wajib"})
print(ok.status, ok.body)
print(bad.status, bad.body)

Output:

201 {'judul': 'OOP Python'}
400 {'error': 'judul wajib'}
Request = method + path + body Client GET /api/buku Handler HttpResponse status + body
Framework nanti hanya menerjemahkan baris “GET /api/buku” menjadi pemanggilan handler.

Method & status — kamus singkat

Method Makna ringan Status tipikal
GET Baca resource; jangan ubah data 200 + daftar/item
POST Buat resource baru 201 dibuat · 400 validasi gagal

Untuk Seri 4 kita fokus GET/POST dulu. PUT/PATCH/DELETE menyusul saat resource punya id stabil.

def ringkas_status(code):
    if code == 200:
        return "OK baca"
    if code == 201:
        return "Created"
    if code == 400:
        return "Bad Request"
    if code == 404:
        return "Not Found"
    if code == 405:
        return "Method Not Allowed"
    return f"lain:{code}"


for c in (200, 201, 400, 404, 405):
    print(c, "->", ringkas_status(c))

Output:

200 -> OK baca
201 -> Created
400 -> Bad Request
404 -> Not Found
405 -> Method Not Allowed

404 dan 405 sering tertukar: path salah vs method salah — kita bedakan lagi di mini-router.

Resource /api/buku — satu URL, dua aksi

Resource perpustakaan kita tetap domain yang sama dengan Capstone. Bedanya: klien berbicara lewat path, bukan menu CLI.

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self):
        self._items = []

    def tambah(self, judul, penulis):
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self):
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status, body):
        self.status = status
        self.body = body


def handle_list(service):
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service, judul, penulis):
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


svc = PerpustakaanService()
print("GET /api/buku ->", handle_list(svc).status)
print("POST kosong ->", handle_create(svc, "  ", "X").status)
print("POST ok ->", handle_create(svc, "REST Ringkas", "Dewi").status)
print("GET lagi ->", handle_list(svc).body)

Output:

GET /api/buku -> 200
POST kosong -> 400
POST ok -> 201
GET lagi -> {'items': [{'judul': 'REST Ringkas', 'penulis': 'Dewi'}]}

Mini router — method + path tanpa Flask

Ini jembatan mental ke decorator Flask: kita “dispatch” request ke handler yang sudah ada.

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self):
        self._items = []

    def tambah(self, judul, penulis):
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self):
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status, body):
        self.status = status
        self.body = body


class HttpRequest:
    def __init__(self, method, path, body=None):
        self.method = method.upper()
        self.path = path
        self.body = body or {}


def handle_list(service):
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service, judul, penulis):
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


def dispatch(service, request):
    if request.path != "/api/buku":
        return HttpResponse(404, {"error": "path tidak dikenal"})
    if request.method == "GET":
        return handle_list(service)
    if request.method == "POST":
        return handle_create(
            service,
            request.body.get("judul", ""),
            request.body.get("penulis", ""),
        )
    return HttpResponse(405, {"error": "method tidak diizinkan"})


svc = PerpustakaanService()
print(dispatch(svc, HttpRequest("GET", "/api/buku")).status)
print(dispatch(svc, HttpRequest("POST", "/api/buku", {"judul": "HTTP", "penulis": "Sari"})).status)
print(dispatch(svc, HttpRequest("GET", "/api/salah")).status)
print(dispatch(svc, HttpRequest("DELETE", "/api/buku")).status)

Output:

200
201
404
405

Perhatikan: DELETE menolak dengan 405 — jujur soal method yang belum kita dukung, bukan “diam-diam 200”.

GET aman diulang, POST berhati-hati

Idempotensi ringan: memanggil GET berkali-kali tidak menambah buku. POST yang sama bisa menambah entri baru — itu normal untuk “create” tanpa id unik dulu.

class BukuItem:
    def __init__(self, judul, penulis):
        self.judul = judul
        self.penulis = penulis

    def as_dict(self):
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self):
        self._items = []

    def tambah(self, judul, penulis):
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self):
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status, body):
        self.status = status
        self.body = body


def handle_list(service):
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service, judul, penulis):
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


svc = PerpustakaanService()
handle_create(svc, "Satu", "A")
print("setelah 1 POST, jumlah=", len(handle_list(svc).body["items"]))
handle_list(svc)
handle_list(svc)
print("setelah 2 GET, jumlah tetap=", len(handle_list(svc).body["items"]))
handle_create(svc, "Satu", "A")
print("POST judul sama lagi, jumlah=", len(handle_list(svc).body["items"]))

Output:

setelah 1 POST, jumlah= 1
setelah 2 GET, jumlah tetap= 1
POST judul sama lagi, jumlah= 2

Pola Dasar — kontrak sebelum framework

  1. 1
    Tentukan resource /api/buku — koleksi, bukan “halaman PHP acak”.
  2. 2
    Pilih method GET baca · POST buat — jangan campur di satu fungsi tanpa beda status.
  3. 3
    Status + body bersama HttpResponse dari Flask/FastAPI (#52) — jangan sukses palsu 200.
  4. 4
    Dispatch tipis HttpRequest + dispatch — bayangan decorator Flask.
  5. 5
    Baru pasang framework Artikel berikutnya: routing Flask nyata — service tetap sama.

Kode lengkap — http_rest_kontrak.py

Simpan dan jalankan: python http_rest_kontrak.py.

"""HTTP/REST ringan di atas stub OOP (Seri 4 #53).
Lanjut ke Flask routing di artikel berikutnya — service/handler tetap dipakai.
"""

from __future__ import annotations


class BukuItem:
    def __init__(self, judul: str, penulis: str) -> None:
        self.judul = judul
        self.penulis = penulis

    def as_dict(self) -> dict:
        return {"judul": self.judul, "penulis": self.penulis}


class PerpustakaanService:
    def __init__(self) -> None:
        self._items: list[BukuItem] = []

    def tambah(self, judul: str, penulis: str) -> BukuItem:
        if not judul.strip():
            raise ValueError("judul wajib")
        item = BukuItem(judul.strip(), penulis.strip() or "Anonim")
        self._items.append(item)
        return item

    def daftar(self) -> list[dict]:
        return [i.as_dict() for i in self._items]


class HttpResponse:
    def __init__(self, status: int, body) -> None:
        self.status = status
        self.body = body


class HttpRequest:
    def __init__(self, method: str, path: str, body: dict | None = None) -> None:
        self.method = method.upper()
        self.path = path
        self.body = body or {}


def handle_list(service: PerpustakaanService) -> HttpResponse:
    return HttpResponse(200, {"items": service.daftar()})


def handle_create(service: PerpustakaanService, judul: str, penulis: str) -> HttpResponse:
    try:
        item = service.tambah(judul, penulis)
    except ValueError as exc:
        return HttpResponse(400, {"error": str(exc)})
    return HttpResponse(201, item.as_dict())


def dispatch(service: PerpustakaanService, request: HttpRequest) -> HttpResponse:
    if request.path != "/api/buku":
        return HttpResponse(404, {"error": "path tidak dikenal"})
    if request.method == "GET":
        return handle_list(service)
    if request.method == "POST":
        return handle_create(
            service,
            str(request.body.get("judul", "")),
            str(request.body.get("penulis", "")),
        )
    return HttpResponse(405, {"error": "method tidak diizinkan"})


def demo() -> None:
    svc = PerpustakaanService()
    r1 = dispatch(svc, HttpRequest("GET", "/api/buku"))
    r2 = dispatch(svc, HttpRequest("POST", "/api/buku", {"judul": "REST", "penulis": "Kindo"}))
    r3 = dispatch(svc, HttpRequest("POST", "/api/buku", {"judul": "  ", "penulis": "X"}))
    r4 = dispatch(svc, HttpRequest("GET", "/api/buku"))
    r5 = dispatch(svc, HttpRequest("DELETE", "/api/buku"))
    print(r1.status, r1.body)
    print(r2.status, r2.body)
    print(r3.status, r3.body)
    print(r4.status, r4.body)
    print(r5.status, r5.body)


if __name__ == "__main__":
    demo()

Output yang diharapkan:

200 {'items': []}
201 {'judul': 'REST', 'penulis': 'Kindo'}
400 {'error': 'judul wajib'}
200 {'items': [{'judul': 'REST', 'penulis': 'Kindo'}]}
405 {'error': 'method tidak diizinkan'}

Kesalahan umum

Gejala Penyebab tipikal Perbaikan
Semua “sukses” 200 Tidak membedakan create/validasi 201 / 400 seperti stub Flask/FastAPI (#52)
GET menambah data Side-effect di baca Mutasi hanya di POST (atau method tulis lain)
404 tidak pernah muncul Path salah tetap diproses Cek path di dispatch
Method aneh diabaikan Fallthrough ke GET Kembalikan 405 dengan jelas
Langsung belajar decorator Lompat kontrak Stub dulu — Flask belakangan

Latihan singkat

  1. Tambah path /api/buku/cari (GET) yang filter judul — tetap lewat dispatch.
  2. Tolak POST tanpa key judul di body dengan 400 (beda dari judul kosong).
  3. Catat di kertas: mapping mana dari dispatch yang akan jadi @app.get / @app.post di Flask.

FAQ singkat

Apakah ini REST “murni”?
Tidak perlu. Cukup resource + method + status yang konsisten untuk API mini.

Kenapa belum install Flask?
Supaya kontrak HTTP jelas sebelum sintaks framework. Pola sama dengan stub di Flask/FastAPI (#52).

Apa bedanya 404 dan 405?
404 = path tidak dikenal. 405 = path dikenal, method tidak diizinkan.

Lanjut ke mana?
Artikel berikutnya di Seri 4: Flask routing & JSON — memasang dispatch ke pintu nyata (belum hardlink sampai artikel itu live).

Kesimpulan & langkah berikutnya

HTTP/REST adalah bahasa bersama client dan server. Stub OOP-mu sudah berbicara bahasa itu; framework hanya penerjemah.

Artikel ini adalah #53 (ini) — pembuka Seri 4 Web Lanjut setelah pintu OOP web (#52).

Seri 4 progress: langkah #53 (ini) · 0/8 menuju Capstone API · prasyarat Flask/FastAPI (#52) LIVE · fondasi Tier 2 tetap. Berikutnya: Flask routing & JSON (tanpa hardlink sampai artikel itu live).