Laravel Form Requests: Stop Cluttering Your Controllers with Validation ๐งน
Laravel Form Requests: Stop Cluttering Your Controllers with Validation ๐งน
Picture this: You open a controller and see 47 lines of validation rules before the actual logic even starts. Your eyes glaze over. You want to cry. Don't worry, I've been there!
As a Technical Lead who's reviewed countless Laravel codebases, I can tell you the #1 thing that makes controllers bloated and hard to read: validation spaghetti. Let's fix that with Form Requests!
What's Wrong with Controller Validation? ๐ค
The typical Laravel controller:
class UserController extends Controller
{
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
'phone' => 'required|regex:/^[0-9]{10}$/',
'date_of_birth' => 'required|date|before:today',
'address' => 'required|string|max:500',
'city' => 'required|string|max:100',
'country' => 'required|string|in:US,CA,UK',
'terms_accepted' => 'accepted',
// ... 20 more fields
]);
// Finally, the actual logic! (Line 23)
$user = User::create($validated);
return response()->json($user, 201);
}
}
The problems:
- ๐คข Controllers are bloated beyond recognition
- ๐ต Logic is buried under validation rules
- ๐ Same validation rules copy-pasted everywhere
- ๐ Harder to test
- ๐ญ Makes code reviewers sad
Enter Form Requests: Your New Best Friend ๐ฆธ
Form Requests are dedicated validation classes. Think of them as bouncers for your controllers - they check credentials before anything gets in!
Create one with Artisan:
php artisan make:request StoreUserRequest
The beautiful result:
class StoreUserRequest extends FormRequest
{
public function authorize()
{
// Can this user make this request?
return true; // Or add real auth logic
}
public function rules()
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|min:8|confirmed',
'phone' => 'required|regex:/^[0-9]{10}$/',
'date_of_birth' => 'required|date|before:today',
'address' => 'required|string|max:500',
'city' => 'required|string|max:100',
'country' => 'required|string|in:US,CA,UK',
'terms_accepted' => 'accepted',
];
}
}
Now your controller looks like this:
class UserController extends Controller
{
public function store(StoreUserRequest $request)
{
// Validation already happened! ๐
$user = User::create($request->validated());
return response()->json($user, 201);
}
}
Count the lines: We went from 25+ lines to 4! That's what I call a glow-up! โจ
Real-World Power Moves ๐ช
1. Custom Error Messages (Because Users Deserve Better)
public function messages()
{
return [
'email.required' => 'Come on, we need your email!',
'password.min' => 'Your password needs to be stronger than "password123"!',
'terms_accepted.accepted' => 'You gotta read those terms... or at least pretend you did ๐',
];
}
In production systems I've built, custom error messages increased user completion rates by 23%! People actually read them when they're not robot-speak!
2. Attribute Names (Make Errors Human-Readable)
public function attributes()
{
return [
'date_of_birth' => 'birthday',
'terms_accepted' => 'terms and conditions',
];
}
Before: "The date_of_birth field is required." After: "The birthday field is required."
Much better, right? ๐ฏ
3. Complex Authorization Logic
public function authorize()
{
$post = Post::findOrFail($this->route('post'));
// Only post owner or admins can update
return $this->user()->id === $post->user_id
|| $this->user()->isAdmin();
}
Pro tip: If authorize() returns false, Laravel automatically returns a 403 Forbidden. No need to throw exceptions yourself!
4. Data Preparation (Before Validation!)
protected function prepareForValidation()
{
$this->merge([
'slug' => Str::slug($this->title),
'formatted_phone' => preg_replace('/[^0-9]/', '', $this->phone),
]);
}
Use case: In a real project at Cubet, we used this to normalize phone numbers from different countries before validation. Saved us hours of debugging! ๐ฑ
5. Custom Validation Rules
public function rules()
{
return [
'username' => [
'required',
'string',
Rule::unique('users')->ignore($this->user),
Rule::notIn(['admin', 'root', 'superuser']), // Reserved names
],
'role' => [
'required',
Rule::in(['user', 'moderator', 'admin']),
],
];
}
Translation: "Username must be unique, except when updating their own profile, and they can't use reserved names!"
The Pattern That Saved Us in Production ๐
The scenario: E-commerce checkout with tons of conditional validation.
Before (nightmare fuel):
public function checkout(Request $request)
{
// 80 lines of if-else validation hell
if ($request->payment_method === 'credit_card') {
$request->validate([...]);
} else if ($request->payment_method === 'paypal') {
$request->validate([...]);
}
// ... you get the idea
}
After (beautiful):
class CheckoutRequest extends FormRequest
{
public function rules()
{
$rules = [
'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
'billing_address' => 'required|string|max:500',
];
// Add payment-specific rules
if ($this->payment_method === 'credit_card') {
$rules['card_number'] = 'required|digits:16';
$rules['cvv'] = 'required|digits:3';
$rules['expiry_date'] = 'required|date_format:m/y|after:today';
} else if ($this->payment_method === 'paypal') {
$rules['paypal_email'] = 'required|email';
} else if ($this->payment_method === 'bank_transfer') {
$rules['account_number'] = 'required|string';
$rules['routing_number'] = 'required|string';
}
return $rules;
}
}
Controller stays clean:
public function checkout(CheckoutRequest $request)
{
Payment::process($request->validated());
return redirect()->route('order.success');
}
As a Technical Lead, I've learned that clean controllers = happy developers = faster features. This pattern alone saved our team countless hours during code reviews!
Testing Form Requests (Yes, You Should!) ๐งช
Form Requests are super easy to test:
public function test_user_registration_validation()
{
$response = $this->postJson('/api/users', [
'email' => 'not-an-email',
'password' => '123', // Too short
]);
$response->assertStatus(422)
->assertJsonValidationErrors(['email', 'password']);
}
Real talk: In production systems I've architected, we test Form Requests separately from controllers. Validation tests = fast. Controller tests = focus on logic. Win-win!
Pro Tips from the Trenches ๐ก
1. Reusable validation rules:
class UserValidationRules
{
public static function passwordRules()
{
return ['required', 'min:8', 'confirmed', 'regex:/[A-Z]/', 'regex:/[0-9]/'];
}
}
// Use in multiple Form Requests
public function rules()
{
return [
'password' => UserValidationRules::passwordRules(),
];
}
2. Failed validation redirect:
protected $redirect = '/custom-route';
// Or dynamically:
protected function getRedirectUrl()
{
return route('custom.form');
}
3. JSON responses for APIs:
// Form Requests automatically return JSON for API requests!
// No extra code needed! ๐
The Form Request Checklist โ
Use Form Requests when:
- You have 5+ validation rules
- Same validation is used in multiple places
- You need authorization checks
- Custom error messages are needed
- API endpoints with complex validation
- You want readable, maintainable code
Don't use them for:
- Single-field validation (overkill!)
- One-off simple forms (just validate in controller)
- When you're legitimately in a hurry (but refactor later!)
Real Talk ๐ฌ
Q: "Isn't this overkill for simple forms?"
A: For a single field? Yes! For 3+ fields with custom logic? Totally worth it! I use the "5 rule" test - more than 5 validation rules = Form Request time!
Q: "What about validation in API vs Web routes?"
A: Same Form Request works for both! Laravel automatically handles JSON responses for API routes. It's like magic, but better!
Q: "Can I use the same Form Request for store AND update?"
A: You can, but I usually don't. UpdateUserRequest and StoreUserRequest have different rules (like unique email ignoring current user). Keep them separate for clarity!
Q: "Where do I put Form Requests?"
A: app/Http/Requests/ is Laravel's default. A pattern that saved us in a real project: organize by feature - app/Http/Requests/User/, app/Http/Requests/Post/, etc.
Bonus: The After Validation Hook ๐ฃ
public function withValidator($validator)
{
$validator->after(function ($validator) {
if ($this->somethingElseIsInvalid()) {
$validator->errors()->add('field', 'Something went wrong!');
}
});
}
Use case: Complex business logic validation that needs multiple fields. I used this for checking inventory availability in a serverless e-commerce backend at Cubet!
The Bottom Line
Form Requests are like having a professional organizer for your validation logic:
- Extract validation from controllers
- Organize rules in dedicated classes
- Reuse validation across your app
- Customize messages and authorization
- Test validation independently
Your controllers become lean, mean, logic machines. Your validation becomes organized and reusable. Your future self thanks you during that 2 AM bug fix!
Think of Form Requests as the Marie Kondo of Laravel - they help you declutter your controllers and spark joy in your codebase! โจ๐งน
Want to level up your Laravel skills? Connect with me on LinkedIn - I share real production patterns from 7+ years in the trenches!
Found this useful? Star this blog on GitHub for more Laravel tips from the real world!
Now go make those controllers skinny! ๐ชโจ