Menangani input pengguna dengan WTForms dan Flask-WTF: mendefinisikan form class dengan validators, melindungi dari serangan CSRF, merender field dan menampilkan error, serta praktik validasi server-side yang benar untuk form HTML.

Form adalah gerbang utama input pengguna di web app — registrasi, login, komentar, hingga pengaturan profil. Tanpa validasi yang benar, form menjadi sumber dua masalah besar: data kotor (input tidak valid masuk ke database) dan celah keamanan (CSRF, XSS). Episode 12 membahas WTForms melalui Flask-WTF — library standar untuk definisi form, validasi, dan proteksi CSRF. Kita akan membangun form class, merendernya di template, memvalidasi di server, dan melindunginya dari serangan CSRF.
Validasi manual seperti yang kita tulis di episode 10 (if not data.get("username")) melelahkan dan mudah tidak konsisten. WTForms mendefinisikan form sebagai class dengan tipe field dan validator deklaratif:
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, SubmitField
from wtforms.validators import DataRequired, Email, Length, EqualTo
class RegisterForm(FlaskForm):
username = StringField("Username", validators=[
DataRequired(),
Length(min=3, max=80),
])
email = StringField("Email", validators=[
DataRequired(),
Email(),
])
password = PasswordField("Password", validators=[
DataRequired(),
Length(min=8),
])
confirm = PasswordField("Konfirmasi Password", validators=[
DataRequired(),
EqualTo("password"),
])
submit = SubmitField("Daftar")Setiap field punya label dan daftar validator. Saat form diproses, semua validator dijalankan berurutan dan hasilnya tersimpan di form.errors.
Flask-WTF menyediakan proteksi CSRF (Cross-Site Request Forgery) secara otomatis — asalkan SECRET_KEY ter-set (episode 8):
pip install flask-wtffrom flask_wtf import CSRFProtect
csrf = CSRFProtect()
def create_app(config) -> Flask:
app = Flask(__name__)
app.config.from_object(config)
csrf.init_app(app)
# ... registrasi blueprint
return appApa itu CSRF? Saat pengguna terautentikasi memegang cookie session, situs jahat bisa memaksa browser-nya mengirim request ke aplikasi kalian (misal lewat form atau image hidden) — seolah-olah dari pengguna yang sah. Token CSRF mencegah ini: setiap form wajib membawa token rahasia yang diverifikasi server.
<form method="POST">
{{ form.hidden_tag() }}
<div>
{{ form.username.label }}
{{ form.username(size=20) }}
</div>
<div>
{{ form.email.label }}
{{ form.email() }}
</div>
<div>
{{ form.password.label }}
{{ form.password() }}
</div>
<div>
{{ form.confirm.label }}
{{ form.confirm() }}
</div>
{{ form.submit() }}
</form>form.hidden_tag() merender token CSRF sebagai hidden field. Tanpa ini, request POST akan ditolak 400 Bad Request oleh CSRFProtect.
Di view, form diproses dengan satu idiom standar:
from app.forms import RegisterForm
@main_bp.route("/register", methods=["GET", "POST"])
def register():
form = RegisterForm()
if form.validate_on_submit():
# validasi lolos - data bersih tersedia di form.data
username = form.username.data
email = form.email.data
# simpan ke database...
return redirect(url_for("main.login"))
return render_template("register.html", form=form)validate_on_submit() melakukan dua hal sekaligus: memeriksa request POST dan menjalankan semua validator. Jika ada error, form.errors terisi dan template dirender ulang — error muncul di field yang bersangkutan. Untuk menampilkan error, loop di template:
{% for error in form.username.errors %}
<p class="error">{{ error }}</p>
{% endfor %}WTForms menyediakan validator bawaan yang mencakup kebutuhan umum:
| Validator | Fungsi |
|---|---|
DataRequired() | Field wajib diisi (bukan kosong/spasi) |
Email() | Format alamat email valid |
Length(min, max) | Panjang string dalam rentang |
EqualTo(field) | Harus sama dengan field lain (konfirmasi password) |
NumberRange(min, max) | Nilai numerik dalam rentang |
Regexp(pattern) | Cocok dengan pola regex |
Optional() | Boleh kosong (tetap divalidasi jika terisi) |
Validator custom memakai decorator @validates atau class validator:
from wtforms.validators import ValidationError
class RegisterForm(FlaskForm):
# ...
def validate_username(self, field):
if User.query.filter_by(username=field.data).first():
raise ValidationError("Username sudah dipakai")Method validate_<fieldname> dijalankan otomatis oleh WTForms setelah validator bawaan. Validator custom adalah tempat yang tepat untuk cek unik, format bisnis, dan aturan domain lainnya.
Penting dipahami: validasi server-side tidak bisa digantikan validasi client-side (HTML required, JavaScript).
| Client-side | Server-side | |
|---|---|---|
| Tujuan | UX (cepat, tanpa reload) | Keamanan & integritas data |
| Bisa dilewati? | Ya — mudah | Tidak — wajib |
| Contoh | required, maxlength di HTML | WTForms validators |
Jangan pernah mempercayai validasi client-side: penyerang bisa menulis request langsung tanpa browser. validate_on_submit() di server adalah garis pertahanan terakhir yang tak boleh absen.
Warning
Aturan emas: setiap input yang sampai ke server wajib divalidasi ulang di server, apa pun yang terjadi di browser. Form yang hanya andal pada atribut HTML required akan membiarkan data kotor masuk saat ada request manual — sumber utama data corrupt dan bug yang sulit dilacak.
hidden_tag() hilang → POST ditolak 400. Selalu sertakan di semua form POST.validate_on_submit dipanggil di GET: idiom ini memeriksa method-nya sendiri; jangan paksa dengan validate() di GET.form.data setelah validate_on_submit() lolos.Pada episode 12 ini, kalian telah menguasai form dan validasi server-side.
Inti yang harus dibawa pulang:
FlaskForm) dengan field dan validator deklaratif.form.hidden_tag() wajib untuk proteksi CSRF — jangan pernah dihilangkan.validate_on_submit() memeriksa POST sekaligus menjalankan semua validator.Di episode 13 selanjutnya, kita mengamankan aplikasi: authentication & sessions — login berbasis session dengan Flask-Login, password hashing dengan Werkzeug, user model, dan protected routes. Pastikan model User dari episode 10 masih segar, karena episode 13 membangun di atasnya!