In one sentence

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.

Why it matters

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 big picture

The validation round trip

πŸ“Form submitPOST /posts
πŸ›‚validate()checks every rule
❌A rule failscontroller stops here
↩️Redirect backerrors + old input in session
πŸ–οΈ@error / old()form shows messages
The failure path. If every rule passes, validate() simply returns the clean data and your controller carries on to save and redirect.
Step by step

Validate the "new post" form

  1. Build a form that posts to store

    Every 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>
            
  2. 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');
              }
            
  3. Learn the rules you'll use daily

    Rules can be written as an array (recommended) or a pipe string like 'required|max:255'.

    RulePasses when…Blog use
    requiredpresent and not emptytitle, body
    nullablemay be empty (other rules skip if so)category_id
    string / integer / booleancorrect typeviews, is_featured
    min:3 / max:255length (strings) or size (numbers)title
    emaillooks like an emailnewsletter signup
    unique:posts,slugno other row has this valueslug
    exists:categories,ida matching row existscategory_id
    in:draft,publishedone of the listed valuesstatus
    date / after:todayvalid date / in the futurepublished_at
    confirmedmatches field_confirmationpassword
    arrayan array (check items with tags.*)tags
  4. Show errors and keep old input

    After a failed submit, $errors is available in every view. @error('field') shows a block only when that field failed, with $message inside. 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>
              @enderror
            

    Want a summary at the top of the form too?

    
              @if ($errors->any())
                  <ul>
                      @foreach ($errors->all() as $error)
                          <li>{{ $error }}</li>
                      @endforeach
                  </ul>
              @endif
            
  5. Move rules into a Form Request class

    When rules grow, or are shared between store and update, 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() returning false

    Laravel responds with 403 Forbidden. For anything beyond "is logged in", use a policy instead. See Policies.

Common mistakes

Avoid this, do that

❌ Avoid

          $request->validate(['title' => 'required']);
          Post::create($request->all());
        

all() includes fields you never validated, so the rules protect nothing extra.

βœ… Do

          $data = $request->validate(['title' => 'required']);
          Post::create($data);
        

Use the array that validate() (or validated()) returns: only checked fields get through.

❌ Avoid

          <input name="title" value="">
        

One typo elsewhere wipes everything the user wrote. They will not come back.

βœ… Do

          <input name="title" value="{{ old('title') }}">
        

old() restores the previous input after a failed validation.

πŸ’‘ Pro tip: build rules with the Rule class

For 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.

πŸ‹οΈ Try it yourself
  1. Build the create form above and submit it empty. Confirm both errors appear next to their fields.
  2. Add old() to every input, type a short body, and check the title survives the redirect.
  3. Create StorePostRequest and move the rules into it.
  4. Add a status select that must be in:draft,published.
  5. Write a custom message for body.min.
Recap

Key takeaways

Check yourself

Mini quiz