Kursus Python / Membuat API JSON Flask dan memvalidasi masukan

Flask menghubungkan permintaan HTTP dengan fungsi Python sehingga Anda dapat membuat API JSON kecil. Periksa bentuk objek, kolom wajib, tipe, dan rentang sebelum menyimpan masukan, lalu bedakan keberhasilan dan kegagalan melalui status HTTP. Latihan ini membuat daftar pengeluaran dalam memori untuk dipakai pada komputer lokal.

Menyiapkan lingkungan dan batas latihan lokal

Data pada contoh disimpan dalam list Python. Data hilang ketika proses berakhir dan tidak dibagikan antarproses server yang berdiri sendiri. Ini berbeda dari representasi JSON maupun database persisten. Aplikasi publik memerlukan rancangan penyimpanan, autentikasi, otorisasi, konkurensi, dan server deployment tersendiri.

Aktifkan lingkungan virtual proyek, kemudian pasang Flask dengan interpreter yang sama:

python -m pip install flask

Buat folder latihan baru dan gunakan Python 3.12 atau lebih baru. Simpan program sebagai app.py, bukan flask.py, agar nama modul tidak bertabrakan dengan paket Flask. Contoh diperiksa dengan Flask 3.1.3. Jika port 5000 sedang dipakai, ubah nomor port pada kode serta alamat klien secara bersamaan.

Menyimpan aplikasi API lengkap

Simpan seluruh kode berikut sebagai app.py dengan UTF-8:

from flask import Flask, jsonify, request

app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []

@app.errorhandler(413)
def too_large(error):
    return jsonify(error="request body too large"), 413

@app.get("/api/expenses")
def list_expenses():
    return jsonify(expenses)

@app.post("/api/expenses")
def add_expense():
    data = request.get_json(silent=True)
    if not isinstance(data, dict):
        return jsonify(error="expected JSON object"), 400
    name, amount = data.get("name"), data.get("amount")
    if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
        return jsonify(error="invalid name or amount"), 400
    row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
    expenses.append(row)
    return jsonify(row), 201

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=5000, debug=False)

Jalankan python app.py dalam terminal lingkungan proyek. Server mendengarkan pada 127.0.0.1:5000 dengan debugger dimatikan. Alamat http://127.0.0.1:5000/api/expenses menerima GET untuk membaca daftar; pada proses baru hasilnya list kosong. POST pada jalur yang sama menambahkan satu record.

Untuk POST, isi JSON perlu berupa objek dengan name string tidak kosong dan amount integer nonnegatif. Nama dibersihkan dari spasi tepi. Pembuatan berhasil mengembalikan objek baru dengan status 201; penolakan data menghasilkan objek error dengan status 400. GET daftar berhasil memakai status 200.

Konfigurasi MAX_CONTENT_LENGTH membatasi badan permintaan menjadi 4096 byte. Error handler khusus mengubah penolakan ukuran menjadi JSON dengan status 413. Batas badan satu permintaan bukan batas seluruh memori aplikasi: list masih dapat bertambah melalui banyak permintaan kecil yang sah.

Menjalankan pengujian tanpa server terpisah

Simpan kode lengkap berikut sebagai test_client.py di samping app.py:

from app import app, expenses
expenses.clear()
with app.test_client() as client:
    created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
    assert created.status_code == 201
    for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
        response = client.post("/api/expenses", json=payload)
        assert response.status_code == 400 and response.is_json
    assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
    too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
    assert too_large.status_code == 413 and too_large.is_json
    rows = client.get("/api/expenses")
    assert rows.status_code == 200 and len(rows.get_json()) == 1
    print(created.status_code, rows.get_json()[0]["amount"])
print("validation and 413 checks passed")

Jalankan dengan interpreter lingkungan proyek, tanpa opsi optimasi -O karena skrip memakai assertion:

python test_client.py

Hasil lengkap yang diharapkan adalah dua baris: 201 8000 lalu validation and 413 checks passed. Skrip membuat satu pengeluaran sah, menolak beberapa masukan buruk dan badan berlebih, kemudian memastikan daftar masih hanya berisi satu record. expenses.clear() menyiapkan keadaan awal pada proses pengujian, bukan menghapus data server lain yang kebetulan sedang berjalan.

Flask test client memanggil alur penanganan permintaan di dalam proses pengujian. Anda tidak perlu menjalankan python app.py terlebih dahulu untuk skrip ini. Sebaliknya, hasil test client belum membuktikan port jaringan, reverse proxy, TLS, atau konfigurasi deployment telah benar.

Kasus Status dan dampak
Objek nama lunch dan amount 8000 201, satu record ditambahkan.
Boolean, negatif, nama kosong, atau array 400, daftar tidak bertambah.
JSON rusak 400 dengan kebijakan objek error aplikasi.
Badan permintaan di atas batas 413 dengan error JSON dari handler.
GET daftar 200 dengan list record yang tersimpan dalam proses itu.

Jika pengujian gagal, baca assertion yang gagal dan periksa respons status serta JSON-nya. Jangan mengganti semua status menjadi 200 untuk menghilangkan kegagalan: klien memerlukan perbedaan antara pembuatan berhasil dan data ditolak. Aturan handler dijelaskan dalam penanganan error Flask.

Memeriksa kebijakan data dan umur aplikasi

JSON true menjadi bool Python, dan isinstance(True, int) bernilai benar. Karena itu, contoh memakai type(amount) is int untuk menolak boolean. String angka dan angka pecahan juga ditolak; konversi tidak dilakukan otomatis. silent=True membuat kegagalan parsing JSON menghasilkan None yang kemudian ditolak oleh pemeriksaan objek.

Kode ini belum menetapkan panjang maksimum nama atau batas atas jumlah selain batas praktis parsing badan permintaan. Nilai nol diterima dan kolom tambahan diabaikan. Jika kontrak aplikasi memerlukan kolom yang ketat, rentang nominal, atau ukuran nama tertentu, tentukan aturan tersebut dan tambahkan pengujian. Jangan menganggap validasi dasar sudah mencakup semua aturan bisnis.

ID berasal dari panjang list pada satu proses. Mengirim POST yang sama dua kali menambahkan dua record, bukan mendeteksi duplikat. Cara ini tidak menggantikan identitas yang tahan lama atau sinkronisasi pada layanan dengan beberapa worker. Penyimpanan permanen dapat dibangun dengan aturan transaksi seperti pada SQLite.

Handler JSON hanya meliputi jalur dan jenis kegagalan yang ditangani di sini. URL tidak ditemukan atau metode yang tidak didukung masih dapat menghasilkan halaman error HTML bawaan. Server atau proxy yang menolak permintaan lebih dahulu juga tidak dijamin mengikuti bentuk JSON aplikasi. Lihat MAX_CONTENT_LENGTH untuk batas konfigurasi.

Setelah mencoba server lokal, hentikan dengan Ctrl+C. Mulai ulang dan periksa GET daftar untuk melihat keadaan memori kembali kosong. Pertahankan server pengembangan pada komputer lokal; penggunaan publik memerlukan deployment yang sesuai dan tidak boleh mengaktifkan debugger interaktif. Panduan Flask Quickstart menjelaskan cara menjalankan aplikasi dan batas server pengembangan.