Validation checks every piece of submitted form data against rules you define, and if anything fails, Laravel sends the user back to the form with friendly error messages.
πValidation is passport control. Every traveller (input) is checked against the rules before entering the country (your database). If something's wrong, they're turned back with a note explaining exactly what to fix.
Never trust user input
Clean data
A post without a title or a slug with spaces would break pages later. Catch it at the door.
Security
Browsers can be bypassed. HTML required attributes are a convenience; server-side rules are the real guard.
Kinder UX
Users see what went wrong next to the right field, and what they typed is still there.
The validation round trip
validate() simply returns the clean data and your controller carries on to save and redirect.Validate the "new post" form
-
Build a form that posts to
storeEvery form that changes data needs
@csrf, which adds a hidden token proving the request came from your own site. Without it Laravel replies with a 419 error. (Learn why in CSRF & XSS.)<form method="POST" action="{{ route('posts.store') }}"> @csrf <label for="title">Title</label> <input id="title" name="title" type="text"> <label for="body">Body</label> <textarea id="body" name="body"></textarea> <button type="submit">Publish</button> </form> -
Validate in the controller with
$request->validate()Pass an array of field β rules. If every rule passes, you get back an array containing only the validated fields, which is safe to hand to
create().public function store(Request $request) { $validated = $request->validate([ 'title' => ['required', 'string', 'max:255'], 'body' => ['required', 'string', 'min:20'], 'category_id' => ['nullable', 'exists:categories,id'], ]); $request->user()->posts()->create($validated); return redirect()->route('posts.index'); } -
Learn the rules you'll use daily
Rules can be written as an array (recommended) or a pipe string like
'required|max:255'.Rule Passes when⦠Blog use requiredpresent and not empty title, body nullablemay be empty (other rules skip if so) category_id string/integer/booleancorrect type views, is_featured min:3/max:255length (strings) or size (numbers) title emaillooks like an email newsletter signup unique:posts,slugno other row has this value slug exists:categories,ida matching row exists category_id in:draft,publishedone of the listed values status date/after:todayvalid date / in the future published_at confirmedmatches field_confirmationpassword arrayan array (check items with tags.*)tags -
Show errors and keep old input
After a failed submit,
$errorsis available in every view.@error('field')shows a block only when that field failed, with$messageinside.old('field')refills what the user typed.<label for="title">Title</label> <input id="title" name="title" type="text" value="{{ old('title') }}"> @error('title') <p class="error">{{ $message }}</p> @enderror <label for="body">Body</label> <textarea id="body" name="body">{{ old('body') }}</textarea> @error('body') <p class="error">{{ $message }}</p> @enderrorWant a summary at the top of the form too?
@if ($errors->any()) <ul> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> @endif -
Move rules into a Form Request class
When rules grow, or are shared between
storeandupdate, move them into a Form Request: a dedicated class that validates (and can authorize) before your controller method even starts.php artisan make:request StorePostRequest<?php namespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StorePostRequest extends FormRequest { public function authorize(): bool { return $this->user() !== null; // only logged-in users } public function rules(): array { return [ 'title' => ['required', 'string', 'max:255'], 'slug' => ['required', 'alpha_dash', 'unique:posts,slug'], 'body' => ['required', 'string', 'min:20'], 'category_id' => ['nullable', 'exists:categories,id'], 'tags' => ['array'], 'tags.*' => ['exists:tags,id'], ]; } public function messages(): array { return ['slug.unique' => 'That URL is already taken. Try another slug.']; } }Now just type-hint it. If validation fails, the method never runs.
use App\Http\Requests\StorePostRequest; public function store(StorePostRequest $request) { $request->user()->posts()->create($request->validated()); return redirect()->route('posts.index'); }βΉοΈauthorize()returningfalseLaravel responds with 403 Forbidden. For anything beyond "is logged in", use a policy instead. See Policies.
Avoid this, do that
$request->validate(['title' => 'required']);
Post::create($request->all());
all() includes fields you never validated, so the rules protect nothing extra.
$data = $request->validate(['title' => 'required']);
Post::create($data);
Use the array that validate() (or validated()) returns: only checked fields get through.
<input name="title" value="">
One typo elsewhere wipes everything the user wrote. They will not come back.
<input name="title" value="{{ old('title') }}">
old() restores the previous input after a failed validation.
Rule classFor updates, the slug must be unique except for the current post. Rule::unique('posts', 'slug')->ignore($this->post) handles that cleanly. You'll use it in Update & Delete.
- Build the create form above and submit it empty. Confirm both errors appear next to their fields.
- Add
old()to every input, type a short body, and check the title survives the redirect. - Create
StorePostRequestand move the rules into it. - Add a
statusselect that must bein:draft,published. - Write a custom message for
body.min.
Key takeaways
- Every data-changing form needs
@csrf. $request->validate([...])returns only validated data, or redirects back with errors.@error('field')+$messageshows per-field errors;old('field')refills inputs.- Form Request classes keep rules, messages and authorization out of the controller.
- Save with
validated()data, neverall().