Validation is your application's first line of defense: it ensures incoming data matches your expectations before it ever touches the database. Laravel ships with a very complete validation system, yet many developers pile everything into the controller until it becomes a mess. This article shows how to validate forms cleanly in Laravel 10/11 — from the simplest approach to custom rules.
1. Inline Validation with validate()
The fastest way: call $request->validate() directly in the controller. On failure, Laravel automatically redirects the user back with the error messages and old input.
public function store(Request $request)
{
$data = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'age' => 'nullable|integer|min:17',
]);
User::create($data);
return back()->with('success', 'Saved.');
}
The return value of validate() contains only the fields that passed the rules — safe to pass straight into create().
2. Commonly Used Validation Rules
required/nullable— required or allowed to be empty.email,numeric,integer,string,boolean— data types.min:3/max:255— string length or numeric value.unique:users,email/exists:categories,id— database checks.confirmed— great for passwords (needs apassword_confirmationfield).in:draft,published— value must be one of a list.
3. Form Requests for Clean Controllers
As rules grow, move them into a Form Request class so the controller stays lean:
php artisan make:request StoreArticleRequest
Fill in the authorize() and rules() methods:
public function authorize(): bool
{
return true; // replace with permission logic if needed
}
public function rules(): array
{
return [
'title' => 'required|string|max:150',
'body' => 'required|string',
'status' => 'required|in:draft,published',
'tags' => 'array',
'tags.*' => 'string|max:30',
];
}
Then simply type-hint it in the controller — validation runs automatically before the method executes:
public function store(StoreArticleRequest $request)
{
Article::create($request->validated());
return redirect()->route('articles.index');
}
4. Custom Messages and Attribute Names
The default error messages are in English. Override them with the messages() and attributes() methods in the Form Request:
public function messages(): array
{
return [
'title.required' => 'The title is required.',
'title.max' => 'The title may not exceed 150 characters.',
];
}
public function attributes(): array
{
return [
'title' => 'article title',
];
}
With attributes(), a generic message like "The title field is required" becomes "The article title field is required" — handy for the :attribute placeholder.
5. Custom Validation Rules
For unique logic, create your own rule:
php artisan make:rule Uppercase
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
Use it alongside built-in rules:
'code' => ['required', new Uppercase],
For one-off validation, you can also write a closure inline:
'slug' => [
'required',
function ($attribute, $value, $fail) {
if (str_contains($value, ' ')) {
$fail('The slug may not contain spaces.');
}
},
],
6. Displaying Errors in Blade
Laravel stores errors in an $errors variable that is always available in every view. Display them per field and preserve old input with old():
<input type="text" name="title" value="{{ old('title') }}">
@error('title')
<span class="text-red-500">{{ $message }}</span>
@enderror
To show all errors at the top of the form:
@if ($errors->any())
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
@endif
7. Validating Arrays and Files
Use dot notation with * to validate each element of an array, and file rules for uploads:
$request->validate([
'items' => 'required|array|min:1',
'items.*.name' => 'required|string',
'items.*.qty' => 'required|integer|min:1',
'avatar' => 'required|image|mimes:jpg,png|max:2048',
]);
The max:2048 rule on a file is measured in kilobytes, so the example above caps the image at 2 MB.
8. Form Requests Once Rules Get Complex
When rules start piling up inside a controller, the next step is a Form Request. Rules, messages, and authorization live in one reusable class that can be tested on its own:
php artisan make:request StoreProductRequest
class StoreProductRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()?->can('create', Product::class) ?? false;
}
public function rules(): array
{
return [
'name' => 'required|string|max:120',
'slug' => 'nullable|string|unique:products,slug',
'price' => 'required|numeric|min:0',
'category_id' => 'required|exists:categories,id',
'description' => 'nullable|string',
'is_active' => 'sometimes|boolean',
];
}
public function messages(): array
{
return [
'name.required' => 'A product name is required.',
'price.min' => 'Price cannot be zero.',
'slug.unique' => 'That slug is already taken.',
];
}
public function attributes(): array
{
return ['category_id' => 'category'];
}
protected function prepareForValidation(): void
{
if ($this->filled('slug')) {
$this->merge(['slug' => Str::slug($this->input('slug'))]);
}
}
}
The controller becomes trivial because validation, authorization, and input cleanup all happen before the method runs:
public function store(StoreProductRequest $request)
{
$product = Product::create($request->validated());
return redirect()->route('products.show', $product);
}
prepareForValidation() is the most commonly skipped hook. It runs before the rules execute, making it the cheapest place to normalize input: trim whitespace, slugify, or coerce dates into a consistent format. Because it happens first, the validator only ever sees tidy data. For the full CRUD picture, see building a Laravel 11 CRUD from scratch.
9. Conditional Rules: sometimes, required_if, nullable
Many rules only apply in certain states. Picking the wrong modifier is a common source of validation that is either too loose or needlessly strict:
| Rule | Meaning | Typical use |
|---|---|---|
required | Must be present and non-empty | Name, email, password |
nullable | May be empty, but must match rules if sent | Optional phone number |
sometimes | Validated only when the key is present | Fields that only appear on the edit form |
required_if | Required when another field equals a value | Reason required when status is rejected |
required_with | Required when another field is present | Details required when a product is chosen |
prohibited_if | Forbidden with a given value | Stop users changing their own role |
$request->validate([
'email' => 'required|email|unique:users,email,' . $user->id,
'password' => 'nullable|min:8|confirmed',
'current_password' => 'required_with:password|current_password',
'role' => 'sometimes|in:admin,editor,viewer',
'reason' => 'required_if:status,rejected|nullable|string|max:255',
]);
Two traps hide in this snippet. First, unique on an edit form must exclude the current record, otherwise a user can never save a change without also changing the slug. Second, password needs nullable so the edit form does not demand the old password again. Note how required_if produces a readable message with no custom string: "the reason field is required when status is rejected."
10. Friendly Error Messages
Laravel's default messages are English and name the field verbatim. Fix them per request, or translate once for the whole app:
$request->validate($rules, $messages, $attributes);
// or, application-wide
php artisan lang:publish
Publishing resources/lang/en/validation.php is the more consistent option because it also covers Form Requests and any implicit validation. Translate the rules users actually hit: exists, unique, confirmed, accepted, and password. Those are the confusing ones.
Never surface a raw exception message to users; it leaks column names and looks broken. Link the message to the input for screen readers as well:
<input name="email" aria-invalid="true" aria-describedby="email-error">
@error('email')
<span id="email-error" role="alert">{{ $message }}</span>
@enderror
11. Test the Rules That Actually Break
public function test_name_is_required(): void
{
$this->post('/products', ['price' => 10000])
->assertSessionHasErrors('name');
}
public function test_slug_unique_ignores_itself(): void
{
$product = Product::factory()->create(['slug' => 'coffee']);
$this->actingAs(User::factory()->create())
->put("/products/{$product->id}", ['name' => 'Coffee', 'slug' => 'coffee'])
->assertSessionHasNoErrors();
}
The second test is the one that fails most often. Once you learn to exclude $user->id from a unique rule, it is easy to forget the same fix on a product slug, and users silently lose the ability to save edits. Writing one test per unique rule costs far less than the bug report that follows.
Conclusion
Start with inline validate() for simple forms, then move to Form Requests as rules grow. Keep three habits: use prepareForValidation() to normalize input, choose the right modifier (nullable, sometimes, required_if) instead of piling on extra rules, and translate error messages for real users. Add a test for every unique rule, because that is where validation bugs hide. The result is validation that stays centralized, easy to test, and keeps controllers thin.