Membedah objek request (args, form, json, files), membangun Response secara eksplisit dengan status codes dan redirect, hingga menangani error lewat @app.errorhandler dengan custom 404/500 dan format error API yang konsisten.

Di episode 4 kita memakai request dan jsonify sebatas kebutuhan CRUD. Episode 5 melengkapi gambaran: request punya banyak sumber data (query string, form, JSON, file), response bisa dibangun eksplisit (status code, header, cookie), dan — yang paling sering dilupakan — error handling yang konsisten adalah pembeda aplikasi profesional dari sekadar demo.
Kita akan membahas tiga bagian: memeriksa request, membangun response, dan menangani error secara terpusat.
Objek request (dari flask.request) menyimpan seluruh data masuk. Empat sumber data yang wajib kalian kenali:
| Atribut | Sumber | Kapan Dipakai |
|---|---|---|
request.args | Query string ?key=value | Filter, pagination, pencarian |
request.form | Body application/x-www-form-urlencoded | HTML form (episode 12) |
request.get_json() | Body application/json | REST API |
request.files | Body multipart/form-data | Upload file (episode 19) |
from flask import Flask, jsonify, request
app = Flask(__name__)
@app.get("/search")
def search() -> dict:
q = request.args.get("q", "")
limit = request.args.get("limit", 10, type=int)
return jsonify({"query": q, "limit": limit})
@app.post("/echo")
def echo() -> tuple[dict, int]:
data = request.get_json(silent=True)
if data is None:
return jsonify({"error": "body harus JSON valid"}), 400
return jsonify(data)request.args.get("q", "") mengambil query string dengan default kosong; argumen type=int mengonversi otomatis (gagal konversi jatuh ke default, bukan error). get_json(silent=True) mengembalikan None jika body bukan JSON valid, alih-alih memunculkan error — pasangan sempurna dengan pengecekan manual.
Kembalian view bisa berupa string, tuple, atau objek Response. Tuple memberi kontrol status dan header tanpa kelas baru:
@app.get("/created")
def created() -> tuple[dict, int, dict]:
return (
jsonify({"id": 7, "status": "created"}),
201,
{"Location": "/items/7"},
)Untuk kontrol penuh — termasuk cookie — gunakan kelas Response:
from flask import Response, make_response
@app.get("/health")
def health() -> Response:
resp = make_response('{"status": "ok"}')
resp.status_code = 200
resp.headers["Content-Type"] = "application/json"
resp.headers["Cache-Control"] = "no-store"
return respmake_response() menerima hampir semua bentuk kembalian view dan memberikannya sebagai objek Response yang bisa diubah-ubah. Header seperti Cache-Control penting untuk kontrol caching — akan kita kembangkan di episode 7 dan 15.
Memindahkan pengguna ke URL lain memakai redirect():
from flask import redirect, url_for
@app.post("/login")
def do_login() -> Response:
# validasi berhasil...
return redirect(url_for("dashboard"))url_for() menghasilkan URL dari nama view function — cara yang aman karena jika route berubah, URL otomatis mengikuti. redirect() default memberi status 302; untuk pattern umum pasca-POST, pertimbangkan redirect(url, code=303) atau code=307 untuk mempertahankan method.
Flask memisahkan error dari logika normal lewat exception handler. Ini tiga hal yang wajib dimiliki setiap aplikasi:
from flask import jsonify
@app.errorhandler(404)
def not_found(e: Exception) -> tuple[dict, int]:
return jsonify({"error": "resource tidak ditemukan"}), 404
@app.errorhandler(500)
def internal_error(e: Exception) -> tuple[dict, int]:
return jsonify({"error": "terjadi kesalahan internal"}), 500Semua 404 Not Found — termasuk yang muncul dari route tak dikenal atau converter gagal — kini melewati handler ini. Dampaknya: konsistensi format error di seluruh aplikasi.
Untuk error yang dipicu logika, lemparkan exception bawaan Flask:
from werkzeug.exceptions import NotFound
@app.get("/items/<int:item_id>")
def get_item(item_id: int) -> tuple[dict, int]:
if item_id > 100:
raise NotFound("item di luar jangkauan")
return jsonify({"item_id": item_id})raise NotFound(...) melewati pipeline error handler — jadi pesan custom 404 di atas ikut terpakai tanpa duplikasi logika. Gunakan BadRequest, Unauthorized, Forbidden, dan NotFound dari werkzeug.exceptions sesuai semantik HTTP.
API yang baik punya shape error yang sama di mana pun. Definisikan sekali dan pakai di semua handler:
def error_response(message: str, status: int) -> tuple[dict, int]:
return jsonify({"error": message, "status": status}), status
@app.errorhandler(404)
def not_found(e: Exception) -> tuple[dict, int]:
return error_response("resource tidak ditemukan", 404)
@app.errorhandler(500)
def internal_error(e: Exception) -> tuple[dict, int]:
return error_response("terjadi kesalahan internal", 500)Dengan pola ini, client hanya perlu memeriksa satu bentuk: {"error": ..., "status": ...}. Ini fondasi yang akan kita perkuat di episode 14 (REST API design).
Note
Handler 500 jangan bocorkan detail error internal (traceback, SQL, path file) ke client — itu informasi berharga bagi penyerang. Di produksi, kembalikan pesan generik dan log detail lengkap ke server side. Kita bahas lebih dalam di episode 17.
request: objek request bersifat read-only; gunakan request.get_data()/copy jika perlu memodifikasi.get_json() tanpa silent=True: jika body bukan JSON, Flask melempar BadRequest. Pilih perilaku sesuai kasus.args.get(type=...) salah: type=int mengonversi nilai string; jika string bukan angka, default yang dipakai — bukan exception.Cache-Control yang benar (episode 7).Pada episode 5 ini, kalian telah menguasai siklus request-response dan error handling.
Inti yang harus dibawa pulang:
args, form, get_json(), files — pilih sesuai media type.(body, status, headers) dan kelas Response untuk kontrol penuh.redirect() + url_for() untuk navigasi yang aman terhadap perubahan route.@app.errorhandler(404) dan (500) menyeragamkan format error di seluruh aplikasi.Di episode 6 selanjutnya, kita pindah dari JSON ke templates dengan Jinja2 — syntax dasar, inheritance dan blocks, macros, filters, serta autoescaping untuk melindungi dari XSS. Ini adalah langkah pertama menuju web app dengan halaman HTML sungguhan!