Validation and authorization: two concerns, one class
Every Laravel application validates form input. Most also check whether the user has permission to perform an action. Traditionally, you'd scatter validation rules across your controllers and authorization logic across middleware or policies. Laravel validation with Form Requests: a practical guide consolidates both into a single, reusable class that keeps your controllers clean and your business logic centralized.
We've seen too many controllers bloated with validation chains and gate checks. Form Requests eliminate that friction by providing a dedicated home for both concerns. By day 13 of your Laravel journey, understanding Form Requests transforms how you structure validation and authorization, making your code more maintainable and your controllers more focused.
Generating a Form Request with php artisan make:request
Laravel ships with a handy artisan command that scaffolds a Form Request class in seconds:
php artisan make:request StoreProductRequestThis creates app/Http/Requests/StoreProductRequest.php. Inside, you'll find two methods: authorize() and rules(). Both are stubs by default, waiting for you to implement your business logic.
In a real e-commerce application we worked on, we generated requests for every major action: storing products, updating inventory, processing refunds. Each request class became the single source of truth for what data was acceptable and who could perform the action. Over time, this prevented dozens of bugs because validation logic lived in one predictable place.
You can also generate multiple requests at once or target a specific namespace. The flexibility is there, but start simple: one request per action. If you're building a REST API with many endpoints, consider grouping related requests in subdirectories:
app/Http/Requests/Products/StoreProductRequest.php
app/Http/Requests/Products/UpdateProductRequest.php
app/Http/Requests/Orders/StoreOrderRequest.php
app/Http/Requests/Orders/UpdateOrderRequest.phpThis organization scales well as your application grows and makes it easier to locate request logic when you need to modify validation rules.
Defining validation rules with the rules() method
The rules() method returns an array where keys are field names and values are validation rule strings or arrays:
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreProductRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'sku' => 'required|string|unique:products,sku',
'price' => 'required|numeric|min:0.01',
'description' => 'nullable|string|max:2000',
'category_id' => 'required|integer|exists:categories,id',
'images' => 'nullable|array|max:5',
'images.*' => 'image|mimes:jpeg,png,webp|max:2048',
];
}
}Notice that we define array field rules with dot notation (images.*). This pattern validates each image file individually. Also observe unique:products,sku—this checks the database without writing raw queries.
In our trading platform, we had complex product pricing rules. Instead of scattering those rules across multiple controllers, we centralized them in a single request class. When business requirements changed, we updated one file. Every endpoint that used that request automatically got the new rules.
You can also override the attributes() and messages() methods to customize field names and error messages:
public function attributes(): array
{
return [
'name' => 'product name',
'sku' => 'stock keeping unit',
];
}
public function messages(): array
{
return [
'sku.unique' => 'This SKU is already in our system.',
'price.min' => 'Price must be at least one cent.',
];
}Real-world example: in a travel booking system, we had complex date range validation. Instead of repeating the same date logic in multiple request classes, we created a custom rule:
public function rules(): array
{
return [
'check_in_date' => 'required|date|after:today',
'check_out_date' => 'required|date|after:check_in_date',
'guests' => 'required|integer|min:1|max:10',
];
}
public function messages(): array
{
return [
'check_in_date.after' => 'You cannot book a date in the past.',
'check_out_date.after' => 'Check-out must be after check-in.',
'guests.max' => 'Maximum 10 guests per booking.',
];
}Another edge case: conditional rules. In an update request, you might skip the unique check for the current record:
public function rules(): array
{
return [
'email' => [
'required',
'email',
Rule::unique('users', 'email')
->ignore($this->user()->id),
],
];
}Checking authorization with the authorize() method
The authorize() method returns a boolean. If it returns false, Laravel throws a 403 Forbidden exception:
public function authorize(): bool
{
// Only allow admins or users with product-creation role
return auth()->user()?->can('create products');
}This is cleaner than scattering checks throughout your controller. Consider a SaaS platform where teams manage billing cycles. You might write:
public function authorize(): bool
{
$billCycle = BillCycle::findOrFail($this->route('bill_cycle_id'));
return auth()->user()->belongsToTeam($billCycle->team_id)
&& auth()->user()->hasRole('finance_lead');
}Now your controller doesn't need to repeat this logic. The authorization happens automatically before your controller method even runs.
We once refactored a ride-hailing application where drivers were making unauthorized requests to update rides they didn't own. Using authorize() in the request class, we plugged that hole in one place. Every endpoint using that request immediately became secure.
A critical edge case: what if the resource being authorized doesn't exist? Your authorize() method should handle that gracefully. We recommend throwing a 404 rather than a 403:
public function authorize(): bool
{
$order = Order::find($this->route('order_id'));
if (!$order) {
abort(404);
}
return auth()->user()->id === $order->user_id;
}Another pattern we use: separating authentication from authorization. Always check that the user is authenticated first:
public function authorize(): bool
{
if (!auth()->check()) {
return false;
}
$project = Project::find($this->route('project_id'));
return auth()->user()->canEditProject($project);
}Accessing validated data with $request->validated()
After validation and authorization pass, you retrieve the validated data using validated():
namespace App\Http\Controllers;
use App\Http\Requests\StoreProductRequest;
use App\Models\Product;
class ProductController extends Controller
{
public function store(StoreProductRequest $request)
{
$data = $request->validated();
$product = Product::create($data);
return response()->json($product, 201);
}
}$request->validated() returns only fields that passed validation. This is a security feature: it prevents mass assignment vulnerabilities. If a malicious actor sends an admin field in the request, it won't appear in validated() unless you explicitly included it in your rules.
You can also retrieve specific fields from validated data:
$name = $request->validated('name');
$images = $request->validated('images', []);In a CRM system, we used validated() to pass data directly to service classes. The service class never had to question whether data was clean—it already was. This separation of concerns made testing easier: mock the request, stub validation, and test business logic in isolation.
A practical pattern: merging or transforming validated data before persisting:
public function store(StoreProductRequest $request)
{
$data = $request->validated();
// Transform nested array into separate fields
$data['image_primary'] = $data['images'][0] ?? null;
$data['created_by_user_id'] = auth()->id();
$product = Product::create($data);
return response()->json($product, 201);
}Another scenario: using validated data across multiple database operations. In a travel system, booking a flight also needs to reserve a seat:
public function book(BookFlightRequest $request)
{
$data = $request->validated();
$booking = Booking::create([
'user_id' => auth()->id(),
'flight_id' => $data['flight_id'],
'seat_class' => $data['seat_class'],
]);
Seat::findOrFail($data['seat_id'])
->update(['booking_id' => $booking->id]);
return response()->json($booking, 201);
}Handling edge cases and common mistakes
Forgetting to type-hint the request in your controller: Without the type hint, Laravel can't auto-inject the request, and validation won't trigger. Always write public function store(StoreProductRequest $request), never just $request. We've debugged this issue more times than we'd like to admit.
Calling validate() after using a Form Request: Form Requests validate automatically. Calling $request->validate() again is redundant and confusing. Use the request object as-is.
Overcomplicating authorization: If your authorize() method becomes deeply nested or requires multiple database queries, consider moving it to a policy. Form Requests are for simple permission checks; policies handle complex authorization logic.
Not handling validation errors: By default, Laravel redirects back with errors on validation failure. For APIs, override failedValidation() to return JSON:
use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;
protected function failedValidation(Validator $validator)
{
throw new HttpResponseException(
response()->json([
'message' => 'Validation failed',
'errors' => $validator->errors(),
], 422)
);
}Forgetting to handle empty inputs: In an update request, clients might send only fields they want to change. Use sometimes rule to validate conditionally:
public function rules(): array
{
return [
'email' => 'sometimes|required|email|unique:users',
'phone' => 'sometimes|required|phone',
'address' => 'sometimes|string|max:255',
];
}Missing null checks in authorize(): When fetching a resource by route parameter, always verify it exists before checking authorization:
public function authorize(): bool
{
$article = Article::find($this->route('id'));
// Bad: can cause null pointer errors
// return auth()->user()->id === $article->author_id;
// Good: explicit null check
if (!$article) {
abort(404);
}
return auth()->user()->id === $article->author_id;
}Key takeaways
- Use
php artisan make:request ClassNameto scaffold Form Requests and eliminate scattered validation logic. - Implement
rules()to return an array of Laravel validation rules; use dot notation for array fields. - Implement
authorize()to return a boolean; returnfalseto throw a 403 exception, and abort(404) for missing resources. - Always call
$request->validated()to retrieve only the fields that passed validation and authorization. - Type-hint the Form Request in your controller method signature; Laravel injects and validates automatically.
- Customize error messages and field names with
messages()andattributes()for better user feedback. - For APIs, override
failedValidation()to return JSON errors instead of redirects. - Keep authorization logic simple in Form Requests; move complex logic to dedicated policy classes.
- Use conditional rules with
sometimesin update requests to handle partial data. - Transform or merge validated data before passing to models or service classes.






Comments
Loading comments…