Validation is one of those "small" things that can slowly ruin a codebase. At first, it is just a couple of request()->validate() calls inside your controllers, but as you add more fields and endpoints, you end up with duplicated validation arrays and controllers full of conditionals.
Laravel Form Requests solve this by moving validation and authorization into dedicated classes.
At first, it’s a couple of request()->validate() calls inside controllers.
Then you add more fields, more endpoints, more rules… and suddenly you have:
- duplicated validation arrays,
- controllers full of conditionals,
- inconsistent error messages,
- and “who is allowed to do this?” checks mixed with validation logic.
The Philosophy: Controllers Orchestrate, Form Requests Validate
A Form Request is a custom class that centralizes:
- rules(): Your validation logic.
- authorize(): Who is allowed to perform the action.
- messages(): Custom error messages.
- prepareForValidation(): Normalizing data before rules are applied.
In short:
Controllers orchestrate. Form Requests validate (and can authorize).
Why use Form Requests?
1) Keep controllers clean
Controllers stay focused on what to do, not how to validate.
2) Reuse rules safely
A StorePostRequest and UpdatePostRequest can share or extend rules without copy/paste.
3) Centralize authorization for an action
authorize() can be a great companion to Policies:
- Policy decides resource permissions
- Form Request decides action permission (e.g. “can create?”)
4) Easier testing
Form Requests make it straightforward to test validation and edge cases.
Creating a Form Request
php artisan make:request StoreProjectRequest
You’ll get something like:
// app/Http/Requests/StoreProjectRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [];
}
}
Basic example: store a Project
Form Request
// app/Http/Requests/StoreProjectRequest.php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class StoreProjectRequest extends FormRequest
{
public function authorize(): bool
{
// Option A: keep it simple
return auth()->check();
// Option B: delegate to a Policy (recommended in many apps)
// return $this->user()->can('create', Project::class);
}
public function rules(): array
{
return [
'name' => ['required', 'string', 'min:3', 'max:120'],
'description' => ['nullable', 'string', 'max:2000'],
'status' => ['required', Rule::in(['draft', 'active'])],
'starts_at' => ['nullable', 'date'],
];
}
}
Controller stays small
// app/Http/Controllers/ProjectController.php
use App\Http\Requests\StoreProjectRequest;
class ProjectController
{
public function store(StoreProjectRequest $request)
{
$data = $request->validated();
// ... create logic (model/service/use case)
// Project::create([...$data, 'user_id' => $request->user()->id]);
return redirect()->back()->with('status', 'Project created!');
}
}
Notice the main win:
- validation is not in the controller
-
$request->validated()guarantees only valid fields go through
Improve error messages (optional)
public function messages(): array
{
return [
'name.required' => 'Please provide a project name.',
'name.min' => 'Project name must be at least :min characters.',
];
}
public function attributes(): array
{
return [
'starts_at' => 'start date',
];
}
Normalizing input with prepareForValidation()
Super useful for cleaning data before rules run.
protected function prepareForValidation(): void
{
$this->merge([
'name' => trim((string) $this->input('name')),
]);
}
Common use cases:
- trimming strings,
- converting “on” to boolean,
- mapping legacy keys,
- defaulting missing values.
Advanced pattern: shared rules for Store/Update
Often your update rules differ slightly (e.g. unique constraints).
// app/Http/Requests/UpdateProjectRequest.php
use Illuminate\Validation\Rule;
public function rules(): array
{
$projectId = $this->route('project')?->id;
return [
'name' => [
'required', 'string', 'max:120',
Rule::unique('projects', 'name')->ignore($projectId),
],
'status' => ['required', Rule::in(['draft', 'active', 'archived'])],
];
}
How Form Requests fit with Policies
A clean setup is:
- Policy → resource-level permissions (“can update THIS project?”)
- Form Request → input validation (+ optional action-level checks)
Example:
- Controller calls
$this->authorize('update', $project)(Policy) - Form Request validates fields and shapes the input
This separation keeps your authorization logic explicit and reusable.
Testing validation quickly
Even a few tests can prevent regressions.
public function test_project_name_is_required(): void
{
$response = $this->post('/projects', [
'name' => '',
'status' => 'draft',
]);
$response->assertSessionHasErrors(['name']);
}
Copy/paste checklist
- [ ] Create a Form Request per action (
StoreXRequest,UpdateXRequest) - [ ] Keep controllers thin:
$request->validated() - [ ] Normalize input in
prepareForValidation()when needed - [ ] Use Policies for resource permissions; optionally call them in
authorize() - [ ] Add 3–5 tests for critical validation rules
Wrap-up
Laravel Form Requests are a simple habit that keeps a project clean as it grows:
- controllers stay readable,
- validation rules stay consistent,
- authorization can be clearer,
- and refactors become safer.
Top comments (0)