Master Laravel validation by moving logic into Form Requests. Learn to keep your API controllers thin, handle custom errors, and streamline your workflow.
Previously in this course, we covered resource controllers and API responses, where we learned to transform model data into consistent JSON. In this lesson, we shift our focus to the input side of the request lifecycle. Specifically, we'll learn how to move validation logic out of our controllers and into dedicated Form Request classes to maintain a clean, readable codebase.
In a typical API, your controllers should be responsible for orchestrating the request—receiving the input, passing it to a service or repository, and returning a response. When you include validator logic directly inside the controller methods, you violate the Single Responsibility Principle.
As your project board grows, a TaskController might need validation for creating, updating, and assigning tasks. If all that logic sits in the controller, it quickly becomes unreadable. Form Requests allow us to encapsulate these rules, ensuring that by the time a controller method is executed, the data is already guaranteed to be valid.
Laravel provides an Artisan command to generate these classes. Let's create one for our task creation endpoint:
Bashphp artisan make:request StoreTaskRequest
This creates a file in app/Http/Requests. Inside, you’ll find two primary methods: authorize() and rules(). For an API, the authorize() method is where you'd check if the authenticated user has permission to perform the action (though we will revisit authorization in later modules).
Here is how we define our validation logic for a new task:
PHPnamespace App\Http\Requests; use Illuminate\Foundation\Http\FormRequest; class StoreTaskRequest extends FormRequest { public function authorize(): bool { return true; #6A9955">// We'll handle authorization via Policies later } public function rules(): array { return [ 'title' => 'required|string|max:255', 'description' => 'nullable|string', 'project_id' => 'required|exists:projects,id', 'due_date' => 'nullable|date|after:today', ]; } }
By default, when validation fails, Laravel redirects users back to the previous page. Since we are building a REST API, we need it to return a JSON response instead. We achieve this by overriding the failedValidation method within our StoreTaskRequest class.
PHPuse Illuminate\Contracts\Validation\Validator; use Illuminate\Http\Exceptions\HttpResponseException; protected function failedValidation(Validator $validator) { throw new HttpResponseException(response()->json([ 'message' => 'The given data was invalid.', 'errors' => $validator->errors(), ], 422)); }
By throwing an HttpResponseException, we short-circuit the request lifecycle immediately, ensuring the controller never even sees the invalid input.
Now that our logic is encapsulated, our controller becomes significantly thinner. We simply type-hint the StoreTaskRequest in our method signature:
PHPpublic function store(StoreTaskRequest $request) { #6A9955">// The request is already validated here! $task = $this->taskService->createTask($request->validated()); return response()->json($task, 201); }
This pattern—often discussed as a best practice for clean controller validation—keeps our logic modular and reusable.
UpdateTaskRequest using Artisan.title is optional but has a minimum length of 5 characters if provided.failedValidation override in your new request class to return a 422 status code with your custom JSON structure.TaskController@update method to use this new request class.validated(): Many developers attempt to use $request->all() inside the controller. Always use $request->validated() to ensure you are only working with the data that passed your rules, preventing mass-assignment vulnerabilities.authorize(): Don't try to handle complex business logic in the authorize() method. Keep it simple; use Laravel Policies for domain-specific checks.failedValidation, the API will return HTML error pages if the Accept: application/json header is missing. Always override this method to guarantee a consistent API contract.We've moved validation logic out of the controller and into specialized Form Request classes. By overriding failedValidation, we ensure our API always returns a consistent JSON response when inputs are malformed. This keeps our controllers thin and our code easy to test, following the patterns we established when we started implementing the service layer.
Up next: We will secure our endpoints by implementing custom middleware to verify project ownership and prevent cross-user data leaks.
Learn to build custom middleware in Laravel to enforce resource ownership. Secure your API routes by verifying user access before controllers ever execute.
Read moreLearn how to use database transactions in Laravel to ensure your data remains consistent. Stop partial updates by mastering atomic operations with DB::transaction.
Handling API Validation and Form Requests
Introduction to Laravel Events and Listeners
Asynchronous Processing with Queues
Job Chaining and Batching
Feature Testing Fundamentals
Mocking Services and Repositories in Tests
Testing Events and Jobs
Database Factories and Seeding
API Versioning Strategies
Advanced Request Filtering and Sorting
Handling File Uploads in REST APIs
Real-time Notifications with Broadcasting
Using Observers for Model Lifecycle Hooks
Implementing Policies for Authorization
Customizing Authentication Guards
Rate Limiting API Endpoints
Eloquent Performance Optimization
Caching Strategies for Performance
Using Traits for Code Reuse
Advanced Dependency Injection with Service Providers
Command Line Tools with Artisan
Scheduled Tasks and Cron Jobs
Integrating Third-Party Services
Handling Webhooks
Logging and Monitoring
Database Migrations Best Practices
Advanced Testing: Integration Tests
Testing API Authentication
Code Quality and Static Analysis
Project Structure for Large Applications
Environment and Configuration Management
Deploying Laravel Applications
Database Indexing Strategies
Using Value Objects
Strategy Pattern for Business Rules
Advanced Queue Monitoring
Building a Search API
Handling Concurrency and Race Conditions
API Documentation with OpenAPI
Testing with Test Doubles
Implementing Multi-Tenancy
Refactoring Legacy Code
Using Middleware for Feature Flags
Building Reusable Packages
Performance Profiling
Secure API Design
Event Sourcing Concepts