Skip to main content
David Dew Mallick

David Dew Mallick

ProjectsExperienceSkillsBlog
Back to blog
form request validationcustom validation ruleslaravel conditional validationlaravel validation errors

Laravel Validation: Bail, Custom Rules & Best Practices

Master Laravel validation: bail, custom rules, form requests, and clean 422 API errors, with code examples that keep bad data out for good.

Sep 16, 2026 · 6 min read · Updated Oct 11, 2026

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. Add bail to stop at the first failure per field, or stopOnFirstFailure() 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. exists and unique each 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 $request->validate()

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 validate() directly

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.

<?php

Above: 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_if:field,value

Required when another field equals a value

Payment method drives card fields

required_with:field1,field2

Required if any listed field is present

Address line 2 requires line 1

exclude_unless:field,value

Removes the key from validated data unless the condition holds

Keep payloads clean for the persistence layer

Rule::when(condition, rules)

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.

DD

David Dew Mallick

Intermediate Software Engineer

GitHubLinkedInEmail

More posts

  • Oct 28, 2026 · 8 min read

    Database Sharding: When One Postgres Stops Working

    A decision guide for when database sharding pays off, the failure modes that push you to it, and how to shard without breaking SQL joins and transactions.

  • Oct 28, 2026 · 7 min read

    Database Best Practices: Primary Keys That Scale

    Pick integer, UUID, or UUIDv7 primary keys using index size and write-rate thresholds, plus a reproducible test that shows how random UUIDs bloat B-trees.

On this page

  • How Does Laravel Validation Work Under the Hood?
  • When Should You Use a Form Request for Laravel Validation?
  • How Do You Write Custom Validation Rules in Laravel?
  • How Do You Handle Laravel Conditional Validation?
  • How Should You Return Laravel Validation Errors in APIs?
  • How Does Bail Work in Laravel Validation?
  • Laravel Validation FAQ
  • What does bail do in Laravel validation?
  • When should you use a form request instead of inline validation?
  • How do you test custom validation rules?
Back to top

End of record

Back to top↑

David Dew Mallick

Dhaka, Bangladesh

Contact

  • david.dew.mallick@g.bracu.ac.bd
  • GitHub
  • LinkedIn

2026 David Dew Mallick

  • Privacy
  • Terms
  • Cookies