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: Rules, Custom Checks & Best Practices

Master Laravel validation with custom rules, form requests, and real-time techniques to keep bad data out of your PostgreSQL database and APIs for good.

Sep 16, 2026 · 6 min read

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.

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:

DD

David Dew Mallick

Software Engineer

I build AI-driven SaaS infrastructure and backend systems with Laravel, AWS, and SQL, and write about the engineering decisions behind them.

GitHubLinkedInEmail

More posts

  • Sep 22, 2026 · 8 min read

    Laravel Middleware: A Practical Guide to Request Flow

    Laravel middleware controls every request your app handles. Learn to write, register, and order middleware for auth, throttling, and request mutation.

  • Sep 19, 2026 · 8 min read

    Laravel Caching Strategies That Cut DB Load

    Laravel caching strategies that cut database load: Redis drivers, cache tags, atomic locks, and stampede protection with real code.

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