Pada aplikasi Laravel tradisional, alur validasi server-side biasanya terasa jelas: request masuk, divalidasi, jika gagal pengguna di-redirect kembali dengan pesan error dan old input. Saat memakai Inertia.js, pola ini tetap berlaku, tetapi cara error tersebut sampai ke komponen frontend perlu dipahami dengan benar agar implementasinya rapi dan konsisten.
Artikel ini fokus pada alur validasi server-side antara Laravel dan Inertia.js, khususnya untuk kasus yang cukup sering muncul: satu halaman profil yang memiliki dua form sekaligus, misalnya form update biodata dan form ubah password. Di situ, named error bag menjadi sangat penting karena error dari satu form tidak boleh tampil di form lain.
Kita akan membahas bagaimana Laravel menangani validasi dengan FormRequest, bagaimana custom message disusun, bagaimana redirect back dan old input bekerja, serta bagaimana menampilkan error di komponen Inertia secara konsisten dan mudah dipelihara.
Memahami Alur Validasi Inertia.js dengan Laravel
Secara konsep, Inertia tidak menggantikan validasi Laravel. Inertia hanya menjadi jembatan antara backend dan frontend. Jadi alurnya tetap seperti ini:
- Pengguna mengirim form dari komponen Inertia.
- Request masuk ke controller Laravel atau FormRequest.
- Laravel menjalankan validasi.
- Jika validasi gagal, Laravel melakukan redirect back sambil membawa error dan old input ke session.
- Inertia membaca hasil redirect tersebut dan mengirimkan data error ke halaman yang dirender ulang.
Artinya, Anda tidak perlu memindahkan logika validasi utama ke frontend. Validasi frontend boleh ada untuk pengalaman pengguna, tetapi validasi server-side tetap sumber kebenaran utama.
Catatan penting: Pada Inertia, error validasi umumnya tersedia lewat
page.props.errorsatau lewat helper form sepertiuseForm. Karena itu, struktur penamaan field dan error harus konsisten antara backend dan komponen.
Menggunakan FormRequest untuk Validasi yang Rapi
Untuk kasus nyata, sebaiknya pisahkan validasi ke FormRequest daripada menulis rule langsung di controller. Keuntungannya:
- Controller lebih bersih.
- Rule validasi mudah diuji dan dipelihara.
- Custom message dan otorisasi bisa ditempatkan di satu lokasi.
Misalnya kita punya dua request berbeda: satu untuk update profil, satu untuk ubah password.
FormRequest untuk update biodata
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class UpdateProfileRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:100'],
'email' => [
'required',
'email',
Rule::unique('users', 'email')->ignore($this->user()->id),
],
'bio' => ['nullable', 'string', 'max:500'],
];
}
public function messages(): array
{
return [
'name.required' => 'Nama wajib diisi.',
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
'email.unique' => 'Email tersebut sudah digunakan akun lain.',
'bio.max' => 'Bio maksimal 500 karakter.',
];
}
}FormRequest untuk ubah password
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class UpdatePasswordRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}
public function rules(): array
{
return [
'current_password' => ['required', 'current_password'],
'password' => ['required', 'confirmed', Password::defaults()],
];
}
public function messages(): array
{
return [
'current_password.required' => 'Password lama wajib diisi.',
'current_password.current_password' => 'Password lama tidak sesuai.',
'password.required' => 'Password baru wajib diisi.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
];
}
}Dengan pola ini, setiap form punya aturan dan pesan error yang spesifik. Ini penting saat halaman memiliki lebih dari satu form dengan field yang serupa atau sama-sama sensitif.
Mengapa Named Error Bag Penting pada Beberapa Form di Satu Halaman
Masalah umum di halaman profil adalah adanya beberapa form dalam satu komponen. Misalnya:
- Form update biodata:
name,email,bio - Form ubah password:
current_password,password,password_confirmation
Tanpa named error bag, semua error validasi akan masuk ke bag default. Pada kasus sederhana mungkin tidak terasa. Namun begitu halaman memiliki banyak form, ada beberapa risiko:
- Komponen sulit membedakan error milik form mana.
- Field dengan nama sama di dua form dapat saling mengganggu.
- Tampilan error menjadi membingungkan pengguna.
- Logika frontend menjadi penuh kondisi khusus.
Solusinya adalah memberikan bag terpisah, misalnya profile untuk biodata dan password untuk ubah password.
Controller dengan validateWithBag
Jika Anda memvalidasi langsung di controller, Laravel menyediakan validateWithBag(). Namun untuk contoh yang lebih rapi, kita tetap gunakan FormRequest dan ketika perlu menempatkan error ke bag tertentu, kita bisa melempar ValidationException dengan bag yang sesuai.
Salah satu cara praktis adalah memanfaatkan validator manual di controller untuk endpoint yang memang membutuhkan bag khusus.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
use Inertia\Inertia;
class ProfileController extends Controller
{
public function edit(Request $request)
{
return Inertia::render('Profile/Edit', [
'user' => $request->user(),
]);
}
public function updateProfile(Request $request)
{
$validated = Validator::make($request->all(), [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'unique:users,email,' . $request->user()->id],
'bio' => ['nullable', 'string', 'max:500'],
], [
'name.required' => 'Nama wajib diisi.',
'email.required' => 'Email wajib diisi.',
'email.email' => 'Format email tidak valid.',
'email.unique' => 'Email tersebut sudah digunakan akun lain.',
])->validateWithBag('profile');
$request->user()->update($validated);
return back()->with('success', 'Profil berhasil diperbarui.');
}
public function updatePassword(Request $request)
{
$validated = Validator::make($request->all(), [
'current_password' => ['required', 'current_password'],
'password' => ['required', 'confirmed', Password::defaults()],
], [
'current_password.required' => 'Password lama wajib diisi.',
'current_password.current_password' => 'Password lama tidak sesuai.',
'password.confirmed' => 'Konfirmasi password tidak cocok.',
])->validateWithBag('password');
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('success', 'Password berhasil diperbarui.');
}
}Di contoh ini, masing-masing action mengembalikan error ke bag yang berbeda. Hasilnya, komponen Inertia dapat menampilkan error secara terpisah dan jauh lebih presisi.
Kapan tetap memakai FormRequest penuh?
Jika Anda ingin tetap memakai FormRequest sekaligus named bag, ada beberapa pendekatan lanjutan seperti menyesuaikan validator atau melempar exception dengan bag tertentu. Namun dalam praktik sehari-hari, banyak tim memilih kompromi yang sederhana:
- FormRequest untuk endpoint tunggal atau halaman dengan satu form.
- Validator::make()->validateWithBag() untuk halaman multi-form yang membutuhkan bag berbeda secara eksplisit.
Pilihan ini bukan soal paling elegan secara teori, tetapi soal keterbacaan dan kemudahan debugging.
Redirect Back, Old Input, dan Perilaku Khusus pada Password
Pada validasi gagal, Laravel biasanya menjalankan redirect back sambil membawa:
- error validation
- old input dari request sebelumnya
Inilah yang membuat form dapat terisi kembali setelah gagal validasi. Pada aplikasi Blade klasik, data ini biasanya diakses lewat helper old(). Di Inertia, konsepnya sedikit berbeda karena komponen frontend memegang state sendiri. Jika Anda memakai helper form dari Inertia, nilai input yang sudah diketik umumnya tetap ada di state komponen saat response validasi kembali.
Meski begitu, memahami konsep old input tetap penting karena sumber datanya tetap berasal dari mekanisme redirect Laravel. Untuk field umum seperti name, email, atau bio, mempertahankan nilai lama biasanya diinginkan.
Namun ada pengecualian penting: field password. Demi keamanan dan pengalaman yang lebih aman, jangan berasumsi password lama atau password baru akan dipertahankan seperti input biasa. Sebaiknya kosongkan field password setelah submit, terutama saat berhasil.
Praktik yang disarankan: pertahankan old input untuk form biodata, tetapi reset field sensitif seperti
current_password,password, danpassword_confirmation.
Menampilkan Error Secara Konsisten di Komponen Inertia
Konsistensi tampilan error adalah hal yang sering diabaikan. Padahal, saat proyek membesar, pola error yang berbeda-beda antar komponen akan menyulitkan pemeliharaan.
Gunakan komponen kecil untuk input dan error, misalnya komponen InputError yang hanya bertugas menerima pesan dan menampilkannya jika ada.
Contoh komponen halaman profil
<script setup>
import { useForm, usePage } from '@inertiajs/vue3'
const page = usePage()
const profileForm = useForm({
name: page.props.user.name ?? '',
email: page.props.user.email ?? '',
bio: page.props.user.bio ?? '',
})
const passwordForm = useForm({
current_password: '',
password: '',
password_confirmation: '',
})
const submitProfile = () => {
profileForm.post(route('profile.update'), {
preserveScroll: true,
})
}
const submitPassword = () => {
passwordForm.post(route('profile.password.update'), {
preserveScroll: true,
onSuccess: () => passwordForm.reset(),
})
}
</script>
<template>
<section>
<h2>Biodata</h2>
<form @submit.prevent="submitProfile">
<input v-model="profileForm.name" type="text" />
<div v-if="$page.props.errors.profile?.name" class="text-red-600">
{{ $page.props.errors.profile.name }}
</div>
<input v-model="profileForm.email" type="email" />
<div v-if="$page.props.errors.profile?.email" class="text-red-600">
{{ $page.props.errors.profile.email }}
</div>
<textarea v-model="profileForm.bio" />
<div v-if="$page.props.errors.profile?.bio" class="text-red-600">
{{ $page.props.errors.profile.bio }}
</div>
<button :disabled="profileForm.processing">Simpan Biodata</button>
</form>
</section>
<section>
<h2>Ubah Password</h2>
<form @submit.prevent="submitPassword">
<input v-model="passwordForm.current_password" type="password" />
<div v-if="$page.props.errors.password?.current_password" class="text-red-600">
{{ $page.props.errors.password.current_password }}
</div>
<input v-model="passwordForm.password" type="password" />
<div v-if="$page.props.errors.password?.password" class="text-red-600">
{{ $page.props.errors.password.password }}
</div>
<input v-model="passwordForm.password_confirmation" type="password" />
<button :disabled="passwordForm.processing">Ubah Password</button>
</form>
</section>
</template>Poin penting dari contoh di atas:
- Setiap form punya state terpisah.
- Error diambil dari bag yang sesuai:
errors.profiledanerrors.password. - Field password di-reset saat sukses.
preserveScrollmembantu agar posisi halaman tidak meloncat setelah submit.
Jika Anda tidak memakai named bag, struktur akses error akan lebih datar. Namun untuk halaman multi-form, pendekatan tersebut cepat menjadi rapuh.
Common Mistakes dan Tips Debugging
1. Semua error muncul di tempat yang salah
Penyebab paling umum adalah semua validasi memakai bag default. Solusinya: pastikan endpoint yang berbeda memakai named error bag yang berbeda juga.
2. Error tidak muncul di komponen
Periksa beberapa hal berikut:
- Nama field di backend harus sama dengan yang dibinding di frontend.
- Pastikan Anda membaca bag yang benar, misalnya
errors.profile.name, bukanerrors.name. - Pastikan response benar-benar berupa redirect validation, bukan JSON manual yang memotong alur Inertia.
3. Password tetap terisi setelah submit
Jangan mengandalkan perilaku default. Reset field password secara eksplisit setelah sukses, dan bila perlu juga setelah gagal tergantung kebijakan UI Anda.
4. FormRequest terasa sulit dipakai untuk multi-form
Itu wajar. Di halaman dengan beberapa form, kebutuhan named bag kadang membuat validator manual lebih langsung dan mudah dipahami. Jangan memaksakan satu pola untuk semua kasus.
5. Pesan error tidak konsisten
Standarkan gaya penulisan pesan, misalnya:
- gunakan bahasa yang sama di seluruh aplikasi,
- hindari campuran istilah teknis dan istilah bisnis tanpa alasan,
- simpan custom message dekat dengan validasinya agar mudah dirawat.
Rekomendasi Implementasi untuk Proyek Nyata
Jika Anda membangun halaman Inertia yang memiliki lebih dari satu form, pola berikut biasanya paling aman:
- Pisahkan endpoint untuk setiap form.
- Gunakan validator atau FormRequest yang spesifik untuk setiap aksi.
- Gunakan named error bag untuk mencegah tabrakan error.
- Pisahkan state form di komponen frontend.
- Buat komponen error kecil agar tampilan konsisten.
- Reset field sensitif setelah operasi sukses.
Untuk halaman profil, contoh pembagian yang baik adalah:
POST /profileatauPATCH /profileuntuk biodata dengan bagprofilePUT /profile/passworduntuk password dengan bagpassword
Pola ini jelas, mudah diuji, dan meminimalkan kebingungan saat tim lain membaca kode.
Penutup
Validasi server-side pada Inertia.js dengan Laravel pada dasarnya tetap mengikuti kekuatan utama Laravel: rule yang terpusat, redirect back, error dari session, dan old input. Yang membedakan adalah bagaimana data tersebut dikonsumsi oleh komponen frontend.
Untuk halaman sederhana, validasi default mungkin sudah cukup. Tetapi untuk halaman dengan beberapa form seperti profil pengguna, named error bag adalah alat yang sangat penting agar error tidak saling tercampur. Dikombinasikan dengan FormRequest atau validator yang tepat, custom message yang jelas, dan pola tampilan error yang konsisten, Anda akan mendapatkan alur validasi yang lebih mudah dipahami, lebih aman, dan lebih nyaman digunakan.
Jika harus diringkas dalam satu prinsip: biarkan Laravel tetap menjadi sumber kebenaran validasi, lalu tampilkan hasilnya di Inertia dengan struktur yang disiplin.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!