How to Build Laravel Forms with Validation in 2025

Form — a small word, but very useful. Whether you’re collecting feedback, taking orders on an e-commerce site, editing blog posts, or a...

Karan Datwani
Karan Datwani
Share:

Form — a small word, but very useful. Whether you’re collecting feedback, taking orders on an e-commerce site, editing blog posts, or a full CRUD system, forms are needed everywhere.

It’s not just about HTML. Good forms need solid validation, old input persistence, and clear error messaging. Laravel's built-in features make form handling and validation smoother—for users and for your future self.

What Every Basic Form Should Include

Every form should include:

  • CSRF protection (@csrf) to prevent malicious submissions.
  • HTTP methods (POST for creates, and Spoofing @method('PUT/PATCH') for updates)
  • The correct action URL (using named routes to beat hardcoding URLs)
  • Fields with name attributes matching model and validation
  • old() helper to repopulate data on validation errors
  • Field-level error messages for clarity.

Basic form template:

<form action="{{ route('posts.update', $post) }}" method="POST">
    @csrf
    @method('PUT')
    <div class="mb-3">
        <label for="title" class="form-label">Title</label>
        <input type="text" class="form-control @error('title') is-invalid @enderror" 
               id="title" name="title" value="{{ old('title', $post->title) }}">
        @error('title')
            <div class="invalid-feedback">{{ $message }}</div>
        @enderror
    </div>
    <button type="submit" class="btn btn-primary">Update</button>
</form>

Clean Validation with FormRequest Classes

This approach keeps your controllers clean and validation logic separate:

php artisan make:request StorePostRequest

Define rules in app/Http/Requests/StorePostRequest.php:

public function rules() {
    return [
        'title' => 'required|unique:posts|max:255',
        'slug' => 'required|alpha_dash',
        'body' => 'required',
    ];
}

Use in controller, Laravel auto-validates when you send the request:

public function store(StorePostRequest $request) {
    // Validated data automatically available
    Post::create($request->validated());
}

Quick Validation (Without Request Class)

Sometimes, you want validation right in the controller:

$request->validate([
    'title' => 'required|max:255',
    'body' => 'required',
]);

Good for one-off validations or small forms — but gets messy fast for complex logic.


Show Errors Where They Happen (Bootstrap 5 Style)

Each field should show its own validation error. Pair Bootstrap’s is-invalid class with Blade’s @error directive:

<div class="mb-3">
    <label for="title" class="form-label">Title</label>
    <input type="text" class="form-control @error('title') is-invalid @enderror" 
           id="title" name="title" value="{{ old('title') }}">
    @error('title')
        <div class="invalid-feedback">{{ $message }}</div>
    @enderror
</div>

No vague errors at the top. Clean, inline feedback.


Set Custom Error Messages

Customize validation messages for better user experience:

public function rules()
{
    return [
        'email' => 'required|email|unique:users',
    ];
}

public function messages()
{
    return [
        'email.required' => 'We need your email address!',
        'email.unique' => 'This email is already registered with us.',
    ];
}

Keeping Old Values (So Users Don’t Rage)

If validation fails, Laravel flashes old input automatically. You just need to use old():

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

For checkboxes and selects:

<input type="checkbox" name="active" {{ old('active') ? 'checked' : '' }}>

This keeps your forms friendly and prevents user frustration.

Note: File inputs don’t persist on validation error — users will need to reselect them.


Handling Unique Validation: The Slug Dilemma

When updating something like a blog post, you often want a slug that’s unique—but the current value causes unique error(slug has already been taken'). To exclude the current record from uniqueness checks, you can write:

use Illuminate\Validation\Rule;

'slug' => [
    'required',
    Rule::unique('posts')->ignore($post->id),
],

// or the shorthand version
'slug' => 'required|unique:posts,slug,' . $post->id,

This tells Laravel: “Make sure this is unique… unless it’s the current one”.

Note: the Rule::unique() is more future-proof, readable, and chainable.


Array Fields Handling and Validation

Laravel supports validating and retrieving structured array inputs using dot notation and wildcards.

1. Nested Arrays

Useful for repeating fields like multiple contacts.

HTML:

<input name="contacts[0][name]" value="{{ old('contacts.0.name') }}">
<input name="contacts[0][email]" value="{{ old('contacts.0.email') }}">

Validation:

'contacts.*.name' => 'required|string|max:255',
'contacts.*.email' => 'required|email|unique:contacts,email',

Access in Controller:

foreach ($request->input('contacts', []) as $contact) {
    $name = $contact['name'];
    $email = $contact['email'];
}

2. Flat Arrays (e.g., tags, checkboxes):

Great for multiple selections like tags or categories.

HTML:

<input type="checkbox" name="tags[]" value="news">
<input type="checkbox" name="tags[]" value="tech">

Validation:

'tags' => 'required|array',
'tags.*' => 'string|in:news,tech,sports',

Access in Controller:

$tags = $request->tags; // ['news', 'tech']

File Upload & Validation

Laravel makes file handling easy with simple validation rules. Add enctype to support file uploads.

<form method="POST" action="{{ route('upload') }}" enctype="multipart/form-data">
    ...
</form>
// validate
'image' => 'required|file|mimes:jpg,png|max:2048'

// and get the file
$image = $request->file('image');

For Multiple Files:

<input type="file" name="attachments[]" multiple>
// validate
'attachments.*' => 'file|mimes:pdf|max:5120'

// and get the files
foreach ($request->file('attachments') as $file) {
    $file->store('uploads');
}

Build Forms Using Array (Dynamic Form Generation)

For reusable or admin-style forms, define fields in an array and loop:

// In your controller
public function create()
{
    $formFields = [
        ['name' => 'name', 'label' => 'Product Name', 'type' => 'text', 'required' => true],
        ['name' => 'price', 'label' => 'Price ($)', 'type' => 'number', 'step' => '0.01', 'required' => true],
        ['name' => 'description', 'label' => 'Description', 'type' => 'textarea', 'rows' => 4],
    ];

    return view('products.create', compact('formFields'));
}

In your blade template:

<form action="{{ route('products.store') }}" method="POST">
    @csrf
    
    @foreach($formFields as $field)
        <div class="mb-3">
            <label for="{{ $field['name'] }}" class="form-label">
                {{ $field['label'] }}
                @if(isset($field['required']) && $field['required'])
                    <span class="text-danger">*</span>
                @endif
            </label>
            
            @if($field['type'] === 'textarea')
                <textarea 
                    name="{{ $field['name'] }}" 
                    id="{{ $field['name'] }}" 
                    class="form-control @error($field['name']) is-invalid @enderror"
                    rows="{{ $field['rows'] ?? 3 }}"
                >{{ old($field['name']) }}</textarea>
            @else
                <input 
                    type="{{ $field['type'] }}" 
                    name="{{ $field['name'] }}" 
                    id="{{ $field['name'] }}" 
                    value="{{ old($field['name']) }}"
                    class="form-control @error($field['name']) is-invalid @enderror"
                    {{ isset($field['required']) && $field['required'] ? 'required' : '' }}
                    {{ isset($field['step']) ? 'step='.$field['step'] : '' }}
                >
            @endif
            
            @error($field['name'])
                <div class="invalid-feedback">{{ $message }}</div>
            @enderror
        </div>
    @endforeach
    
    <button type="submit" class="btn btn-primary">Save Product</button>
</form>

Great for rapid development and maintaining large forms like in the admin panels.


Bonus(1): Add Real-Time Feedback with Alpine.js

Client-side validation improves UX, and Alpine lets us set it up quickly:

<div x-data="{ name: '' }" class="mb-3">
    <label for="name" class="form-label">Product Name</label>
    <input 
        type="text" 
        x-model="name" 
        name="name"
        class="form-control" 
        :class="{ 'is-invalid': name.length > 0 && name.length < 3 }"
    >
    <div x-show="name.length > 0 && name.length < 3" class="invalid-feedback">
        Name must be at least 3 characters.
    </div>
</div>

Remember, client-side validation is for user experience - always validate on the server too!

Bonus(2): Build Admin Forms 10x Faster with Backpack

Tired of building repetitive admin forms? Backpack for Laravel saves you hours of form development and reduces it to minutes. This admin panel package gives you:

  • Instant CRUDs forms for any model with a single command.
  • 40+ Field Types from text inputs to rich editors, it's all built-in.
  • File Uploading without complex setup.
  • Set up everything without touching HTML/CSS or JS.
  • No learning curve as it uses Laravel's existing concepts.
  • Customizable UI with theme support.
  • Role & Permission Management without breaking a sweat.
  • No Frontend coding required while maintaining full customization freedom.
  • Relationship field that just work for any relation type.
  • Multi-language support is also built-in.

Instead of wasting hours on repetitive work, Backpack lets you focus on what really matters. It's the fastest way to build form-heavy admin panels, and it’s FREE to use, offering many core features. You can check out the demo here to see what can be built with it and experience it's features live. Next time you're building admin forms, remember there's a better way to build admin panels without sacrificing flexibility.

Final Word

Building forms in Laravel is straightforward once you understand these core concepts. By combining proper validation and preserving user input, you'll create a seamless experience that keeps your data clean and your users happy.

Laravel gives you everything you need to build forms that feel right—for users and for developers.

Recap:

  • Form Request classes for clean validation
  • Blade directives for smart templating
  • Inline errors for polished UI
  • Dynamic generation for repetitive forms
  • old() for restoring user input
  • Array fields, file uploads, and even real-time feedback

Remember, The best forms disappear for users while giving you bulletproof data. Now go build forms that users love to fill out! 🚀

Got a favorite form tip? Share it with fellow readers in the comments!

Want to receive more articles like this?

Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.

Reactions & Comments

What do you think about this?

Latest Articles

Wondering what our community has been up to?