Bab 36 — Web Development: Membuka Warung Data di Internet
Seri I5 — Aplikasi Pertama: Web, Peta, dan Dasbor
Semua skrip bab ini ada di folder kode/bab36/. Di versi web, Anda juga bisa mengunduh semuanya sekaligus (zip).
Sampai Bab 35, program Anda hidup di satu komputer: komputer Anda sendiri. Padahal data kebun, kolam, dan kandang sering dibutuhkan orang lain: penyuluh di kantor, petugas di lapangan, atau aplikasi di ponsel. Cara paling umum untuk berbagi adalah membuka warung data: program Anda duduk menunggu pesanan (“tolong data kolam A”), lalu menjawab. Itulah web server dan API (Application Programming Interface, “pintu” yang dipakai program lain untuk meminta data).

Cara kerja web dalam satu paragraf
Setiap interaksi web adalah pesanan dan jawaban. Klien (browser, aplikasi, atau skrip Python Anda) mengirim permintaan berisi metode dan alamat, misalnya GET /kolam/A. Server membalas dengan kode status dan isi. Isinya biasanya halaman HTML atau data JSON (teks berformat seperti dict, Bab 14).
| Metode | Artinya | Contoh |
|---|---|---|
GET |
Minta data | Lihat daftar kolam |
POST |
Tambah data baru | Catat kolam baru |
PUT / PATCH |
Ubah data | Perbarui jumlah ikan |
DELETE |
Hapus data | Hapus kolam |
| Kode status | Artinya |
|---|---|
200 |
Berhasil |
201 |
Berhasil dibuat (setelah POST) |
404 |
Tidak ditemukan |
409 |
Bentrok (mis. kode sudah ada) |
422 |
Isian tidak lolos pemeriksaan |
REST API hanyalah kebiasaan menata alamat dan metode itu: alamat menyebut benda (/kolam/A), metode menyebut tindakan.
Kode di bab ini tidak menyalakan server untuk ditunggu terus-menerus. Kita memakai klien uji (peniru browser yang menelepon aplikasi langsung dari dalam Python). Hasilnya sama persis, tetapi cepat, aman, dan keluarannya tetap. Satu skrip (07) menyalakan server asli sebentar di thread lalu mematikannya.
Flask: warung paling kecil
Flask adalah kerangka kerja web yang ringan: Anda mulai dengan beberapa baris, lalu menambah sendiri sesuai kebutuhan. Setiap alamat dihubungkan ke sebuah fungsi dengan decorator @app.get(...) (dari Bab 31).
01_flask_dasar.py
# Bab 36 - Flask: aplikasi web terkecil, diuji tanpa menyalakan server
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.get("/")
def beranda():
return "Halo dari kebun!"
@app.get("/sapa/<nama>") # bagian <nama> dari alamat masuk ke fungsi
def sapa(nama):
return {"pesan": f"Selamat pagi, {nama}"} # dict otomatis menjadi JSON
@app.get("/cari")
def cari():
komoditas = request.args.get("komoditas", "semua") # ?komoditas=kopi
return jsonify(dicari=komoditas)
klien = app.test_client() # peniru browser; tidak membuka port jaringan
r = klien.get("/")
print(r.status_code, r.content_type, r.get_data(as_text=True))
r = klien.get("/sapa/Budi")
print(r.status_code, r.content_type, r.get_json())
print(klien.get("/cari?komoditas=kopi").get_json())
r = klien.get("/tidak-ada")
print(r.status_code, r.status)200 text/html; charset=utf-8 Halo dari kebun!
200 application/json {'pesan': 'Selamat pagi, Budi'}
{'dicari': 'kopi'}
404 404 NOT FOUND
"/"mengembalikan teks, jadi jenis isinyatext/html. Mengembalikan dict otomatis menjadi JSON (application/json).<nama>pada alamat adalah parameter alamat: isiBudimasuk ke fungsi sebagai variabelnama.request.argsmembaca parameter query, bagian setelah tanda?di alamat.- Alamat yang tidak terdaftar menghasilkan
404 NOT FOUNDotomatis. app.test_client()adalah klien uji: ia memanggil aplikasi tanpa membuka port jaringan.
REST API dengan Flask
Sekarang warung yang bisa mencatat: daftar kolam disimpan di sebuah dict (di memori, jadi hilang saat program berhenti; penyimpanan tetap datang di bab-bab berikutnya).
02_flask_rest.py
# Bab 36 - REST API dengan Flask: daftar kolam (disimpan di memori)
from flask import Flask, abort, jsonify, request
app = Flask(__name__)
kolam = {"A": {"jenis": "lele", "jumlah": 800}, "B": {"jenis": "nila", "jumlah": 500}}
@app.get("/kolam")
def daftar():
return jsonify(kolam)
@app.get("/kolam/<kode>")
def satu(kode):
if kode not in kolam:
abort(404, description=f"kolam {kode} tidak ada")
return jsonify(kolam[kode])
@app.post("/kolam/<kode>")
def tambah(kode):
isi = request.get_json() # tubuh permintaan (JSON)
kolam[kode] = {"jenis": isi["jenis"], "jumlah": isi["jumlah"]}
return jsonify(kolam[kode]), 201 # 201 = Created
@app.delete("/kolam/<kode>")
def hapus(kode):
kolam.pop(kode, None)
return "", 204 # 204 = berhasil, tanpa isi
@app.errorhandler(404)
def tidak_ada(galat):
return jsonify(galat=galat.description), 404
k = app.test_client()
print("GET /kolam ->", k.get("/kolam").status_code, sorted(k.get("/kolam").get_json()))
r = k.post("/kolam/C", json={"jenis": "lele", "jumlah": 650})
print("POST /kolam/C ->", r.status_code, r.get_json())
print("GET /kolam/C ->", k.get("/kolam/C").get_json())
print("DELETE /kolam/C ->", k.delete("/kolam/C").status_code)
r = k.get("/kolam/C")
print("GET /kolam/C ->", r.status_code, r.get_json())GET /kolam -> 200 ['A', 'B']
POST /kolam/C -> 201 {'jenis': 'lele', 'jumlah': 650}
GET /kolam/C -> {'jenis': 'lele', 'jumlah': 650}
DELETE /kolam/C -> 204
GET /kolam/C -> 404 {'galat': 'kolam C tidak ada'}
Telusuri lima baris keluaran itu: GET daftar menghasilkan 200 dengan kode A dan B; POST menambah kolam C dan dijawab 201; DELETE dijawab 204 (berhasil, tanpa isi); GET kolam C sesudah dihapus dijawab 404 dengan pesan yang kita tulis lewat errorhandler. Perhatikan: Flask tidak memeriksa isi POST kita. Bila jumlah hilang, kode kita akan berhenti dengan KeyError. Pemeriksaan itu kita tulis sendiri. Di sinilah FastAPI unggul.
FastAPI: tipe data yang bekerja untuk Anda
FastAPI memakai type hint (Bab 33) dan Pydantic (Bab 34) untuk memeriksa data otomatis dan membuat dokumentasi sendiri. Fungsinya boleh biasa (def) atau async (Bab 35).
03_fastapi_dasar.py
# Bab 36 - FastAPI: parameter alamat dan parameter query dengan tipe data
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/")
def beranda():
return {"pesan": "Halo dari kebun!"}
@app.get("/kolam/{kode}")
def kolam(kode: str, jumlah_min: int = 0): # kode dari alamat; jumlah_min dari ?jumlah_min=
return {"kode": kode, "jumlah_min": jumlah_min}
@app.get("/panen/{blok}")
async def panen(blok: str): # fungsi async boleh (lihat Bab 35)
return {"blok": blok, "hasil_kg": 245.5}
klien = TestClient(app)
print(klien.get("/").json())
print(klien.get("/kolam/A?jumlah_min=500").json())
print(klien.get("/panen/A").json())
# Tipe data diperiksa otomatis: "banyak" bukan bilangan bulat
r = klien.get("/kolam/A?jumlah_min=banyak")
print(r.status_code)
print(r.json()["detail"][0]["msg"]){'pesan': 'Halo dari kebun!'}
{'kode': 'A', 'jumlah_min': 500}
{'blok': 'A', 'hasil_kg': 245.5}
422
Input should be a valid integer, unable to parse string as an integer
jumlah_min: int = 0 berarti “bilangan bulat, bawaan 0”. Saat kita mengirim ?jumlah_min=banyak, FastAPI menolak dengan kode 422 dan pesan jelas (Input should be a valid integer...) tanpa satu baris pemeriksaan pun yang kita tulis.
Tubuh permintaan dengan model Pydantic
04_fastapi_pydantic.py
# Bab 36 - FastAPI + Pydantic: tubuh permintaan yang divalidasi
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class KolamBaru(BaseModel):
jenis: str
jumlah: int = Field(gt=0)
ph_air: float = Field(ge=0, le=14)
class KolamKeluar(BaseModel): # bentuk jawaban
kode: str
jenis: str
jumlah: int
gudang: dict[str, KolamBaru] = {}
@app.post("/kolam/{kode}", response_model=KolamKeluar, status_code=201)
def tambah(kode: str, kolam: KolamBaru):
gudang[kode] = kolam
return {"kode": kode, **kolam.model_dump()} # ph_air tidak ikut keluar: disaring response_model
@app.get("/kolam/{kode}", response_model=KolamKeluar)
def baca(kode: str):
if kode not in gudang:
raise HTTPException(status_code=404, detail=f"kolam {kode} tidak ada")
return {"kode": kode, **gudang[kode].model_dump()}
k = TestClient(app)
r = k.post("/kolam/A", json={"jenis": "lele", "jumlah": 800, "ph_air": 7.2})
print(r.status_code, r.json())
r = k.post("/kolam/B", json={"jenis": "nila", "jumlah": -5, "ph_air": 17})
print(r.status_code)
for galat in r.json()["detail"]:
print(" ", galat["loc"], "->", galat["msg"])
r = k.get("/kolam/Z")
print(r.status_code, r.json())201 {'kode': 'A', 'jenis': 'lele', 'jumlah': 800}
422
['body', 'jumlah'] -> Input should be greater than 0
['body', 'ph_air'] -> Input should be less than or equal to 14
404 {'detail': 'kolam Z tidak ada'}
KolamBaruadalah model Pydantic untuk data masuk:jumlahharus lebih dari 0 danph_airantara 0 dan 14.KolamKeluaradalah bentuk jawaban.response_modelmenyaring isi jawaban:ph_airtidak ikut keluar pada baris pertama keluaran. Ini cara aman agar data internal tidak bocor.- Pada kolam B, dua aturan dilanggar sekaligus, dan FastAPI melaporkan keduanya:
jumlahdanph_air, lengkap dengan lokasinya (['body', 'jumlah']). HTTPExceptionmengubah masalah kita menjadi kode status yang tepat (404).
Dokumentasi yang tertulis sendiri
05_fastapi_dokumentasi.py
# Bab 36 - FastAPI membuat dokumentasi API sendiri (OpenAPI)
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI(title="API Kebun", version="0.1")
@app.get("/kolam/{kode}", summary="Ambil satu kolam", tags=["kolam"])
def kolam(kode: str):
"""Mengembalikan data satu kolam menurut kodenya."""
return {"kode": kode}
@app.get("/panen", summary="Daftar panen", tags=["panen"])
def panen(blok: str | None = None):
return []
k = TestClient(app)
skema = k.get("/openapi.json").json()
print("judul:", skema["info"]["title"], "| versi:", skema["info"]["version"])
for alamat, metode in skema["paths"].items():
for nama, rincian in metode.items():
print(f"{nama.upper():4s} {alamat:15s} {rincian['summary']}")
print("halaman /docs (Swagger UI) ada:", k.get("/docs").status_code == 200)
print("halaman /redoc ada:", k.get("/redoc").status_code == 200)judul: API Kebun | versi: 0.1
GET /kolam/{kode} Ambil satu kolam
GET /panen Daftar panen
halaman /docs (Swagger UI) ada: True
halaman /redoc ada: True
Dari kode saja, FastAPI membuat skema OpenAPI (standar penjelasan API) di /openapi.json, serta halaman dokumentasi interaktif /docs (Swagger UI) dan /redoc. Penyuluh atau pengembang aplikasi ponsel bisa mencoba API Anda dari browser tanpa membaca kode.
Untuk membuka dokumentasi itu di komputer Anda, jalankan server sungguhan di terminal: uvicorn nama_berkas:app --reload, lalu buka http://127.0.0.1:8000/docs. Kita tidak menjalankannya di bab ini karena server menunggu tanpa akhir; itu tugas terminal Anda.
Pengantar Django
Django adalah kerangka kerja “serba ada” (batteries included): sistem pengguna, panel admin, basis data (ORM), dan templat HTML sudah tersedia. Cocok untuk situs besar yang penuh halaman dan data. Proyek Django sungguhan dibuat dengan perintah django-admin startproject nama yang menghasilkan beberapa berkas. Perintah itu tidak kita jalankan di sini. Sebagai perkenalan, berikut proyek Django satu berkas:
06_django_mini.py
# Bab 36 - Django: proyek satu berkas (hanya untuk perkenalan)
import django
from django.conf import settings
settings.configure(
DEBUG=False,
SECRET_KEY="contoh-saja-bukan-rahasia",
ROOT_URLCONF=__name__, # daftar alamat ada di berkas ini
ALLOWED_HOSTS=["testserver"],
)
django.setup()
from django.http import JsonResponse
from django.test import Client
from django.urls import path
def beranda(request):
return JsonResponse({"pesan": "Halo dari Django"})
def kolam(request, kode):
return JsonResponse({"kode": kode, "jenis": request.GET.get("jenis", "?")})
urlpatterns = [path("", beranda), path("kolam/<str:kode>/", kolam)]
klien = Client()
print(klien.get("/").json())
print(klien.get("/kolam/A/?jenis=lele").json())
print(klien.get("/tidak-ada/").status_code){'pesan': 'Halo dari Django'}
{'kode': 'A', 'jenis': 'lele'}
404
settings.configure(...) mengganti berkas settings.py; urlpatterns adalah daftar alamat; Client adalah klien uji milik Django. Bagian SECRET_KEY di sini hanya teks contoh. Pada proyek nyata, kunci itu harus rahasia dan dibaca dari variabel lingkungan (Bab 34).
| Flask | FastAPI | Django | |
|---|---|---|---|
| Gaya | Mikro, bebas | Modern, berbasis tipe | Serba ada |
| Validasi data | Tulis sendiri | Otomatis (Pydantic) | Formulir dan model |
| Dokumentasi API | Tambahan | Otomatis | Tambahan |
| Cocok untuk | Prototipe, layanan kecil | API data dan model | Situs lengkap dengan admin |
Kita memakai FastAPI untuk proyek bersambung karena tujuan kita adalah API data.
Server asli, sebentar saja
Klien uji tidak melewati jaringan. Untuk membuktikan bahwa aplikasi benar-benar bisa dipanggil lewat HTTP, skrip berikut menyalakan uvicorn (server untuk FastAPI) di sebuah thread (Bab 35), memanggilnya dengan httpx, lalu mematikannya.
07_server_asli_thread.py
# Bab 36 - Menyalakan server asli (uvicorn) di thread, memanggilnya lewat jaringan, lalu mematikannya
import socket
import threading
import time
import httpx
import uvicorn
from fastapi import FastAPI
app = FastAPI()
@app.get("/ping")
def ping():
return {"hidup": True}
with socket.socket() as s: # minta nomor port bebas dari sistem
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
server = uvicorn.Server(uvicorn.Config(app, host="127.0.0.1", port=port, log_level="warning"))
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
while not server.started: # tunggu sampai siap
time.sleep(0.05)
r = httpx.get(f"http://127.0.0.1:{port}/ping") # panggilan HTTP sungguhan lewat jaringan lokal
print(r.status_code, r.json())
server.should_exit = True # minta server berhenti dengan sopan
thread.join(timeout=5)
print("server masih hidup:", thread.is_alive())200 {'hidup': True}
server masih hidup: False
server.should_exit = True meminta server berhenti dengan sopan; thread.join() menunggu sampai selesai. Hasil terakhir False membuktikan server sudah mati. Port diminta dari sistem (angka 0 berarti “pilihkan yang kosong”) supaya tidak bentrok dengan program lain.
Server pengembangan (uvicorn tanpa pengaman, atau app.run() milik Flask) tidak untuk dibuka ke internet. Menyajikan aplikasi dengan aman (HTTPS, proses banyak, pembatasan akses) dibahas di tingkat Advanced.
Proyek Bersambung I5: API data unit usaha
Awal dari “Sistem Informasi Hutan, Kebun, Pertanian, Perikanan, Peternakan” versi web: daftar unit usaha (petak, kolam, kandang) yang bisa dilihat, disaring per sektor, dan ditambah. Luas petak diambil dari angka Bab 38 (data tiruan).
08_proyek_api_unit.py
# Bab 36 - Proyek bersambung I5: API data unit usaha (awal)
from typing import Literal
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
class Unit(BaseModel):
sektor: Literal["hutan", "kebun", "sawah", "kolam", "kandang"]
kode: str
luas_ha: float = Field(gt=0)
app = FastAPI(title="Sistem Informasi Usaha")
unit_db: dict[str, Unit] = {
"H1": Unit(sektor="hutan", kode="H1", luas_ha=9.0),
"S1": Unit(sektor="sawah", kode="S1", luas_ha=3.0),
"S2": Unit(sektor="sawah", kode="S2", luas_ha=2.25),
}
@app.get("/unit", response_model=list[Unit])
def daftar(sektor: str | None = None):
return [u for u in unit_db.values() if sektor is None or u.sektor == sektor]
@app.get("/unit/{kode}", response_model=Unit)
def satu(kode: str):
if kode not in unit_db:
raise HTTPException(404, f"unit {kode} tidak ada")
return unit_db[kode]
@app.post("/unit", response_model=Unit, status_code=201)
def tambah(unit: Unit):
if unit.kode in unit_db:
raise HTTPException(409, f"unit {unit.kode} sudah ada") # 409 = konflik
unit_db[unit.kode] = unit
return unit
k = TestClient(app)
print("sawah:", [u["kode"] for u in k.get("/unit", params={"sektor": "sawah"}).json()])
print("tambah K1:", k.post("/unit", json={"sektor": "kebun", "kode": "K1", "luas_ha": 4.0}).status_code)
print("tambah K1 lagi:", k.post("/unit", json={"sektor": "kebun", "kode": "K1", "luas_ha": 4.0}).status_code)
print("sektor salah:", k.post("/unit", json={"sektor": "empang", "kode": "E1", "luas_ha": 1}).status_code)
print("total luas (ha):", sum(u["luas_ha"] for u in k.get("/unit").json()))sawah: ['S1', 'S2']
tambah K1: 201
tambah K1 lagi: 409
sektor salah: 422
total luas (ha): 18.25
Literal[...] membatasi sektor pada lima pilihan, jadi sektor empang ditolak dengan 422. Kode yang sudah ada ditolak dengan 409. Total luas 18,25 ha adalah 9,0 + 3,0 + 2,25 (unit awal) + 4,0 (K1 yang berhasil ditambah). Data masih di memori; di bab-bab berikutnya API ini akan membaca data peta dan dasbor.
Latihan
- Di
01_flask_dasar.py, tambah alamat/panen/<blok>yang mengembalikan{"blok": ..., "hasil_kg": 245.5}. Uji dengantest_client. - Di
03_fastapi_dasar.py, ubahjumlah_min: intmenjadifloatdan coba?jumlah_min=2.5. Lalu kembalikan keint: apa yang terjadi? - Di
08_proyek_api_unit.py, tambah alamatDELETE /unit/{kode}yang menjawab 204, dan 404 bila kode tidak ada. Uji kedua kasus. - Tantangan: tambah alamat
GET /ringkasanyang mengembalikan total luas per sektor sebagai dict, memakaidefaultdictataudict.get.
Ringkasan Bab
- Web = permintaan (metode + alamat) dan jawaban (kode status + isi, sering JSON). REST menata alamat sebagai benda dan metode sebagai tindakan.
- Flask ringan dan bebas; validasi harus ditulis sendiri.
- FastAPI memeriksa tipe data, memakai Pydantic, menyaring jawaban dengan
response_model, dan membuat dokumentasi/docsotomatis. - Django serba ada, cocok untuk situs lengkap.
- Aplikasi bisa diuji tanpa server memakai klien uji (
test_client,TestClient,Client). - Server pengembangan tidak untuk dibuka ke internet.
Selanjutnya, di Bab 37: Python yang berjalan di dalam browser, tanpa server sama sekali.