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'}
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
Tentukan resource
/api/buku— koleksi, bukan “halaman PHP acak”. -
2
Pilih method GET baca · POST buat — jangan campur di satu fungsi tanpa beda status.
-
3
Status + body bersama
HttpResponsedari Flask/FastAPI (#52) — jangan sukses palsu 200. -
4
Dispatch tipis
HttpRequest+dispatch— bayangan decorator Flask. -
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
- Tambah path
/api/buku/cari(GET) yang filter judul — tetap lewatdispatch. - Tolak POST tanpa key
juduldi body dengan 400 (beda dari judul kosong). - Catat di kertas: mapping mana dari
dispatchyang akan jadi@app.get/@app.postdi 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).