Laravel validation is the cleanest way to verify incoming request data safely and consistently before it ever reaches your controller logic. Well-structured validation gives users clear feedback and stops malformed data from ever touching your database. In this article I walk through the approach I actually use in practice, starting from rule syntax and moving on to Form Request classes, custom rules and error message management.
The quickest path: validate() inside the controller
For small forms, the most practical option is to call $request->validate() directly in the controller method. If the rules pass, execution continues; if they fail, Laravel automatically redirects back and flashes the errors to the session (or returns a 422 for JSON requests).
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'email' => 'required|email',
'age' => 'nullable|integer|min:18',
'tags' => 'array',
'tags.*' => 'string|max:30',
]);
Post::create($validated);
return redirect()->route('posts.index');
}
One important detail: validate() returns only the fields you actually validated. That is also a security win, because it prevents extra, unwanted fields from leaking into the model (mass assignment). Instead of chaining rules with pipes, you can use the array form too; for complex rules that include Rule objects, the array form is far more readable.
Common rules and their logic
Laravel ships with hundreds of built-in rules. In day-to-day work you reach for these the most:
required/nullable— whether a field is mandatory or may be left empty.string,integer,boolean,array,date— type checks.min/max/between— value for numbers, characters for strings, element count for arrays.email,url,uuid— format validation.unique:table,columnandexists:table,column— database-backed checks.confirmed— requirespasswordto match apassword_confirmationfield.
For conditional logic, variants like required_if, required_with and required_without are lifesavers. For example, "card number is required when the payment type is card" becomes a single line: 'card_number' => 'required_if:payment,card'.
Form Request: move validation out of the controller
As rules grow, the controller bloats. The right fix is to create a Form Request class. It collects authorization, rules and messages in one place.
php artisan make:request StorePostRequest
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'slug' => ['required', 'string', Rule::unique('posts')],
'body' => ['required', 'string'],
];
}
}
Then you just type-hint this class in the controller method. Laravel validates the request automatically and never enters the method on failure:
public function store(StorePostRequest $request)
{
Post::create($request->validated());
}
Returning false from authorize() produces a 403; if you handle authorization in a separate layer, you can simply leave this method as return true;. The Form Request approach also makes testing easier, because you can validate the rules in isolation.
Custom rules and closure rules
When the built-in rules fall short, you have two options. For a one-off check, a closure is fastest:
'slug' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
if (str_contains($value, ' ')) {
$fail("The $attribute may not contain spaces.");
}
},
],
If you will reuse the same rule in several places, generating a reusable rule class is cleaner:
php artisan make:rule Uppercase
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
}
Usage is just like any other rule: 'code' => ['required', new Uppercase]. The :attribute placeholder in the message is automatically replaced with the field name.
Managing error messages
The default messages are in English and generic. For friendlier, localized messages, override the messages() and attributes() methods inside the Form Request:
public function messages(): array
{
return [
'title.required' => 'The title field is required.',
'title.max' => 'The title may not exceed :max characters.',
];
}
public function attributes(): array
{
return ['title' => 'post title'];
}
On the Blade side, the @error directive and the old() helper are all you need; old() keeps the user's input from disappearing when the form is re-rendered:
<input name="title" value="{{ old('title') }}">
@error('title')
<span class="error">{{ $message }}</span>
@enderror
In multilingual projects, instead of hard-coding messages in the code, it is more maintainable to use the translation files under the lang/ directory. That way you can serve the same rule set consistently across different languages.
Frequently Asked Questions
What is the difference between validate() and a Form Request?
Both use the same engine. validate() is ideal for quick, small forms; a Form Request collects rules, authorization and messages in a dedicated class, keeping the controller clean and making reuse easy.
How are validation errors returned for API requests?
If the request carries an Accept: application/json header, Laravel does not redirect; it returns a JSON response with a 422 Unprocessable Entity status and an errors object. The frontend can process this structure directly.
Can I validate a field only under a certain condition?
Yes. You can use conditional rules like required_if, or add dynamic rules at runtime with the sometimes() method inside a Form Request.
The validation layer is the security foundation of your project. If you want to make your Laravel validation architecture clean and testable, or to tidy up complex rules in an existing form, get in touch with me.