Skip to main content
Create your own
Lesson illustration

Form Requests for Validation

Hello! Welcome back to our course on mastering Laravel.

In our previous lesson, we focused on making controllers cleaner and more secure by offloading logic to the framework. We used Route Model Binding to automate model fetching and Rate Limiting to protect our endpoints. This theme of writing "thin controllers" is a cornerstone of professional Laravel development.

Today, we'll take that principle a step further. We're going to tackle one of the most common sources of controller clutter: data validation. This lesson directly addresses the learning outcome: Organize controller logic using Form Requests for validation. You'll learn how to extract validation and authorization logic into dedicated, reusable classes, making your controllers incredibly lean and focused.

1. The Problem: "Fat" Controllers

As your application grows, your controller methods for storing or updating data can quickly become bloated. They often end up doing three distinct jobs:

  1. Authorizing that the user can perform the action.
  2. Validating that the incoming data is correct.
  3. Executing the core business logic (e.g., creating a model).

Consider this store method. It's a common pattern, but it mixes several concerns in one place.

// app/Http/Controllers/ProductController.php

public function store(Request $request)
{
    // 1. Authorization Logic
    if (auth()->user()->cannot('create', Product::class)) {
        abort(403);
    }

    // 2. Validation Logic
    $validatedData = $request->validate([
        'name' => 'required|max:255|unique:products',
        'price' => 'required|numeric|min:0',
        'description' => 'nullable|string',
    ]);

    // 3. Business Logic
    $product = Product::create($validatedData);

    return redirect()->route('products.index')->with('success', 'Product created!');
}

While functional, this controller is doing too much. Imagine if the validation rules or authorization checks were more complex. The method would become difficult to read and maintain. This is where Form Requests provide an elegant solution.

2. The Solution: Introducing Form Requests

A Form Request is a dedicated class that contains the validation and authorization logic for a specific request.

Laravel 9 Validation using Form Request: Inline vs. Form Request Class
This image shows the core idea of a Form Request. Logic from the controller's `validate()` method is moved into the `rules()` method of a dedicated request class, and authorization checks are placed in the `authorize()` method.

Here's how it works:

  1. You create a Form Request class using an Artisan command.
  2. You move your authorization logic into its authorize() method.
  3. You move your validation rules into its rules() method.
  4. You "type-hint" this new request class in your controller method signature.

When a request comes in, Laravel's service container detects your Form Request type-hint. It then automatically:

  • Executes the authorize() method. If it returns false, a 403 Forbidden response is sent immediately, and your controller method is never even called.
  • If authorization passes, it runs the validation rules from the rules() method. If validation fails, Laravel automatically redirects the user back with the errors (for web requests) or sends a 422 Unprocessable Entity response with JSON errors (for API requests).

Only if both authorization and validation pass will your controller method finally execute.

3. Creating and Using a Form Request

Let's refactor our "fat" controller from before. The entire process is demonstrated beautifully in the following video from the official Laravel channel.

Unlocking the Power of Form Requests in Laravel

This video provides a complete walkthrough of refactoring a controller to use a Form Request. It's a perfect demonstration of the concepts we've just discussed.

Watch the video from the beginning until 04:47. Pay close attention to these steps: The 'Before' State: The initial controller with inline authorization and validation (00:36). Generating the Class: Using php artisan make:request StorePostRequest (01:50). Moving Authorization: Transferring the gate check into the authorize() method (02:30). Moving Validation: Moving the validation rules array into the rules() method (03:16). Refactoring the Controller: Type-hinting the new StorePostRequest and using the $request->validated() method to get the clean data (03:27).

Let's review the key steps you just saw.

Step 1: Generate the Request Class
In your terminal, you run:
php artisan make:request StoreProductRequest

This creates a new file at app/Http/Requests/StoreProductRequest.php.

Step 2: Implement the authorize() and rules() Methods

// app/Http/Requests/StoreProductRequest.php

namespace App\Http\Requests;

use App\Models\Product;
use Illuminate\Foundation\Http\FormRequest;

class StoreProductRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     */
    public function authorize(): bool
    {
        // Moved from the controller
        return $this->user()->can('create', Product::class);
    }

    /**
     * Get the validation rules that apply to the request.
     */
    public function rules(): array
    {
        // Moved from the controller
        return [
            'name' => 'required|max:255|unique:products',
            'price' => 'required|numeric|min:0',
            'description' => 'nullable|string',
        ];
    }
}

Step 3: Update the Controller
Now, the controller becomes incredibly simple. We replace Illuminate\Http\Request with our new StoreProductRequest.

// app/Http/Controllers/ProductController.php
use App\Http\Requests\StoreProductRequest; // <-- Import it
use App\Models\Product;

public function store(StoreProductRequest $request) // <-- Type-hint it
{
    // Authorization and validation have already passed!

    // Business Logic
    $product = Product::create($request->validated()); // <-- Use validated()

    return redirect()->route('products.index')->with('success', 'Product created!');
}

Notice the call to $request->validated(). This is crucial. It returns an array containing only the data that was defined in your rules() method and passed validation. This prevents mass-assignment vulnerabilities where a malicious user might try to submit extra fields.

4. Advanced Form Request Features

Form Requests can do more than just basic authorization and validation. The official documentation is the best place to explore all the available features.

Validation - Form Request Validation

The official documentation provides a comprehensive overview of Form Requests and all their capabilities. This is an essential reference.

Read the sections titled 'Form Request Validation' and 'Working With Validated Input'. Focus on understanding the purpose of the different methods available, such as: authorize() and rules() (which we've covered). messages() for custom error messages. attributes() for custom attribute names in error messages. prepareForValidation() to sanitize data before validation. after() for adding conditional, complex validation logic. validated() vs safe() for accessing validated data.

Let's highlight two particularly powerful features.

Customizing Error Messages

You can provide more user-friendly error messages by overriding the messages() method.

// In your StoreProductRequest.php
public function messages(): array
{
    return [
        'name.required' => 'A product name is required.',
        'name.unique' => 'A product with this name already exists.',
        'price.min' => 'The price must be a positive number.',
    ];
}

Performing "After" Validation

Sometimes, you have business logic validation that doesn't fit neatly into a standard rule. For example, "A user can't create more than 5 products." The after() hook is perfect for this.

This next video explains and demonstrates this concept clearly.

Unlocking the Power of Form Requests in Laravel

This video shows a practical example of using the after() hook in a Form Request to implement a custom business rule that isn't tied to a single field.

Watch the segment from 05:42 to 08:17. Note how a closure is used within the after() method to access the validator instance and add a custom error if a certain condition is met.

As the video shows, you can add a new method to your Form Request:

// In your StoreProductRequest.php
use Illuminate\Validation\Validator;

public function after(): array
{
    return [
        function (Validator $validator) {
            // Check if the user already has 5 or more products
            if ($this->user()->products()->count() >= 5) {
                // Add an error. This error isn't tied to a specific field,
                // but you can attach it to one if you like.
                $validator->errors()->add(
                    'general', // A generic error key
                    'You have reached the maximum number of products you can create.'
                );
            }
        }
    ];
}

This powerful feature keeps complex business rule validation neatly contained within the relevant request class.

Conclusion

In this lesson, we've seen how to dramatically clean up our controllers by encapsulating logic into Form Requests. This adheres to the "thin controller, fat model" principle and promotes a clear separation of concerns, which is vital for building maintainable, large-scale applications.

Key Takeaways:

  • Form Requests centralize validation and authorization logic for a specific HTTP request.
  • They are created with php artisan make:request and contain authorize() and rules() methods.
  • Laravel automatically resolves Form Requests via the service container when type-hinted in a controller, running the authorization and validation checks before your controller code executes.
  • Always use $request->validated() to get a safe, clean array of the data you need for your business logic.
  • You can extend Form Requests with custom messages, data preparation, and complex "after" validation hooks.

Next Up:

We've now seen how Laravel automatically handles failures in Form Requests by throwing exceptions and generating the appropriate HTTP response. But what about other kinds of errors that might occur in your application? In our next lesson, we will dive into implementing custom exception handling for application-specific errors, giving you full control over how your application responds when things go wrong.

Can't find a good explanation? Sign up and we'll make it for you

Sign up