Bad input is the cheapest bug to prevent and the most expensive to debug. Laravel validation gives you a declarative layer at the edge of your application that rejects malformed data before it reaches your Eloquent models or PostgreSQL tables. This guide covers where to put your rules, how to write reusable custom validation rules, how to handle fields that depend on other fields, and how to return errors cleanly from JSON APIs. It also explains bail, which stops validation at the first failure per field.
Key Takeaway
Validate at the boundary, once. Route all incoming data through form requests, return $request->validated() to your services, and never re-check input deeper in the stack. Every duplicated check is a rule waiting to drift out of sync.
How Does Laravel Validation Work Under the Hood?
When you call $request->validate() or instantiate a Validator, the framework builds a Validator instance from the service container, runs each rule against the input, and collects failures into an Illuminate\Support\MessageBag. If validation fails inside a controller via validate(), Laravel automatically redirects back with errors flashed to the session for web requests, or throws a ValidationException that renders as a 422 JSON response for API requests expecting JSON.
Three execution details matter in production:
Rules run in order. Without
bail, a field with five failing rules produces five error messages. Addbailto stop at the first failure per field, orstopOnFirstFailure()on the validator to stop the whole request.Only validated keys are trusted.
validated()strips anything not covered by a rule, which protects you from mass-assignment surprises.Database-backed rules hit the database.
existsanduniqueeach issue a query, so a 20-field form can fire several queries before your controller runs.
Add 'bail' => true as the first rule on fields with expensive checks. There is no reason to run a unique query when the field already failed required.
Approach | Best For | Reusable Across Controllers | Unit-Testable in Isolation |
|---|---|---|---|
Inline | One-off endpoints, prototypes | No | Only via HTTP tests |
Form request class | Any endpoint you will maintain | Yes | Yes, instantiate and assert |
Rule object | Domain checks reused across forms | Yes | Yes, call |
Closure rule | One-field checks needing local context | No | Via HTTP tests |
When Should You Use a Form Request for Laravel Validation?
Move rules into a dedicated form request the moment a rule set is used twice, exceeds about five fields, or needs authorization. Form request validation keeps controllers to a single line of input handling and makes the rule set injectable anywhere the container resolves dependencies -the same mechanism covered in Laravel Service Container: Bindings, Contextual Injection.
<?phpAbove: a form request pairs authorization with rules, and the controller receives only verified data. This is the core of form request validation and the pattern I default to in every production Laravel codebase -in invoice-processing systems I have tuned, centralizing rules here eliminated an entire class of controller-level drift.
public function store(StoreInvoiceRequest $request): JsonResponse
{
$invoice = Invoice::create(
$request->validated() + ['user_id' => $request->user()->id]
);
ProcessInvoice::dispatch($invoice);
return response()->json(
new InvoiceResource($invoice), 201
);
}Note that validated() returns only keys with rules. If a queued job like ProcessInvoice consumes this data, keep the payload small and follow the retry guidance in Laravel Queue Best Practices for Reliable Background Jobs.
Override prepareForValidation() in your form request to normalize input before rules run -trimming strings, coercing empty strings to null, or converting comma-separated IDs into arrays. Normalizing first means your rules stay simple.
How Do You Write Custom Validation Rules in Laravel?
String rules cover maybe 80 percent of cases. The remaining 20 percent -domain-specific checks like business-day dates, tenant-scoped uniqueness, or checksum verification, belong in custom validation rules. Laravel gives you two tools.
Rule objects for anything reusable or testable:
final class BusinessDay implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
$weekday = (int) date('N', strtotime((string) $value));
if ($weekday > 5) {
$fail("The :attribute must fall on a business day.");
}
}
}
// Usage: 'launch_at' => ['required', 'date', new BusinessDay]Closure rules for one-off checks that need values captured from the surrounding scope, such as comparing against another field or a model already loaded:
'discount_pct' => [
'nullable', 'integer', 'min:0',
function (string $attribute, mixed $value, Closure $fail) use ($plan): void {
if ($value > $plan->max_discount_pct) {
$fail("The discount exceeds the plan limit of {$plan->max_discount_pct}%.");
}
},
],Push closure logic into a rule object as soon as it grows past three lines or appears in a second file. Rule objects are trivial to unit test- construct one, call validate(), and assert the $fail callback fires- which keeps your custom validation rules covered without full HTTP test suites.
How Do You Handle Laravel Conditional Validation?
Real forms have fields that depend on other fields: a shipping address only when delivery is selected, a refund reason only when type is refund. Laravel conditional validation handles this declaratively without hand-rolled if statements in controllers.
Rule | Behavior | Typical Use |
|---|---|---|
| Required when another field equals a value | Payment method drives card fields |
| Required if any listed field is present | Address line 2 requires line 1 |
| Removes the key from validated data unless the condition holds | Keep payloads clean for the persistence layer |
| Applies a rule array conditionally at build time | Role-based rule sets in shared form requests |
public function rules(): array
{
return [
'delivery_method' => ['required', 'in:pickup,courier'],
'address' => [
'exclude_unless:delivery_method,courier',
'required', 'string', 'max:255',
],
'coupon' => [
Rule::when($this->boolean('apply_coupon'), ['required', 'string', 'size:12']),
'exclude_unless:apply_coupon,true',
],
];
}The combination of exclude_unless with conditional requirement is the important part: the field is both skipped from validation and dropped from validated() when irrelevant, so downstream code never sees half-filled keys. For complex flows, ValidationScenario-style branching inside rules() using Rule::when stays readable; resist merging separate endpoints into one endpoint with heavy conditional validation - two focused endpoints usually beat one overloaded one.
How Should You Return Laravel Validation Errors in APIs?
For JSON APIs, a ValidationException renders as HTTP 422 with an errors object keyed by attribute. That default is fine, but most frontends want a consistent envelope and human-readable messages. Customize via an exception handler renderable or by overriding failedValidation() in a base form request:
protected function failedValidation(Validator $validator): void
{
throw new HttpResponseException(
response()->json([
'message' => 'The given data was invalid.',
'errors' => $validator->errors()->toArray(),
], 422)
);
}Overriding failedValidation in a shared base request keeps every endpoint in your API consistent without touching the exception handler. Whichever route you take, keep the 422 status and the per-attribute errors object - API clients and frontend form libraries are built around that contract.
How Does Bail Work in Laravel Validation?
Rules for a field run in order, and by default every failing rule reports its own message. Prefix the list with bail to stop after the first failure for that field:
'email' => ['bail', 'required', 'email', 'unique:users,email']With bail, an empty submission returns only the required message - the format check and the unique database query never run. Skipping database-backed rules on values you already know are malformed is a small but real performance win on high-traffic endpoints. The full treatment, including when to leave bail off so users see every problem at once, lives in Laravel bail validation.
Laravel Validation FAQ
What does bail do in Laravel validation?
Bail stops evaluating rules for a field after the first failure. Without it, one bad value can produce several error messages; with it, you get exactly one per field, and expensive rules like unique never run on values that already failed cheaper checks.
When should you use a form request instead of inline validation?
Use a form request as soon as rules are reused, need authorization logic, or require input normalization in prepareForValidation. Inline $request->validate() is fine for single-rule checks in tiny controllers; everything else belongs in a dedicated request class that can be tested in isolation.
How do you test custom validation rules?
Rule objects are plain classes: instantiate one, call validate() with a failing value, and assert the $fail callback fires. Cover the rule itself with a unit test, then add one HTTP feature test asserting the 422 response shape so the API contract stays pinned.
