Form — a small word, but very useful. Whether you’re collecting feedback, taking orders on an e-commerce site, editing blog posts, or a...
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.
Every form should include:
@csrf
) to prevent malicious submissions.POST
for creates, and Spoofing @method('PUT/PATCH')
for updates)name
attributes matching model and validationold()
helper to repopulate data on validation errorsBasic 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>
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());
}
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.
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.
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.',
];
}
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.
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.
Laravel supports validating and retrieving structured array inputs using dot notation and wildcards.
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'];
}
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']
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');
}
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.
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!
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:
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.
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.
old()
for restoring user inputRemember, 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!
Subscribe to our "Article Digest". We'll send you a list of the new articles, every week, month or quarter - your choice.
What do you think about this?
Wondering what our community has been up to?