Understanding API Request Validation
Request validation is critical for API security and data integrity. Laravel provides powerful validation tools that make it easy to validate incoming data and return consistent error responses in JSON format.
Why Validate API Requests?
Validation protects your application by:
- Preventing invalid data from entering your database
- Protecting against malicious input and injection attacks
- Ensuring data type and format consistency
- Providing clear error messages to API consumers
- Reducing bugs and unexpected behavior
Note: Never trust client input. Always validate and sanitize data on the server side, even if you have client-side validation.
Basic Controller Validation
The simplest way to validate is directly in your controller:
use Illuminate\Http\Request;
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'email' => 'required|email|unique:users,email',
'age' => 'required|integer|min:18|max:120',
'website' => 'nullable|url',
]);
// If validation fails, Laravel automatically returns 422 JSON response
// If it passes, $validated contains only the validated fields
$post = Post::create($validated);
return response()->json([
'success' => true,
'message' => 'Post created successfully',
'data' => $post
], 201);
}
Automatic JSON Error Responses
When validation fails in an API context, Laravel automatically returns a JSON response:
{
"message": "The given data was invalid.",
"errors": {
"title": [
"The title field is required."
],
"email": [
"The email field must be a valid email address.",
"The email has already been taken."
]
}
}
Tip: Laravel detects API requests (routes starting with /api or requests with Accept: application/json header) and automatically returns JSON validation errors.
Common Validation Rules
Laravel provides dozens of built-in validation rules:
| Rule |
Description |
Example |
required |
Field must be present |
'name' => 'required' |
nullable |
Field can be null |
'bio' => 'nullable|string' |
email |
Valid email format |
'email' => 'required|email' |
unique |
Value must be unique in table |
'email' => 'unique:users' |
exists |
Value must exist in table |
'user_id' => 'exists:users,id' |
min/max |
Min/max length or value |
'age' => 'min:18|max:120' |
in |
Value must be in list |
'role' => 'in:admin,user' |
regex |
Match regex pattern |
'phone' => 'regex:/^[0-9]{10}$/' |
Creating Form Request Classes
For complex validation logic, create dedicated Form Request classes:
php artisan make:request StorePostRequest
This creates a class in app/Http/Requests/StorePostRequest.php:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true; // Or implement authorization logic
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'content' => 'required|string|min:100',
'category_id' => 'required|exists:categories,id',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'published_at' => 'nullable|date|after:now',
'featured_image' => 'nullable|url',
];
}
}
Using Form Requests in Controllers
Type-hint the Form Request in your controller method:
use App\Http\Requests\StorePostRequest;
public function store(StorePostRequest $request)
{
// Validation already passed if we reach here
$validated = $request->validated();
$post = Post::create($validated);
return response()->json([
'success' => true,
'data' => $post
], 201);
}
Note: Form Requests are validated before the controller method executes. If validation fails, the response is automatically returned.
Custom Error Messages
Customize validation error messages in your Form Request:
public function messages(): array
{
return [
'title.required' => 'Please provide a post title',
'title.max' => 'Post title cannot exceed 255 characters',
'content.required' => 'Post content is required',
'content.min' => 'Post content must be at least 100 characters',
'category_id.required' => 'Please select a category',
'category_id.exists' => 'Selected category does not exist',
'tags.array' => 'Tags must be provided as an array',
'tags.*.exists' => 'One or more selected tags do not exist',
];
}
Custom Attribute Names
Make error messages more user-friendly by customizing attribute names:
public function attributes(): array
{
return [
'category_id' => 'category',
'published_at' => 'publication date',
'featured_image' => 'featured image URL',
];
}
// Instead of: "The category_id field is required"
// Shows: "The category field is required"
Conditional Validation
Apply rules conditionally based on other fields:
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'is_published' => 'required|boolean',
// Require published_at only if is_published is true
'published_at' => 'required_if:is_published,true|date',
// Require author_notes only if is_published is false
'author_notes' => 'required_if:is_published,false|string',
// Category required unless type is 'draft'
'category_id' => 'required_unless:type,draft|exists:categories,id',
// Tags required with categories
'tags' => 'required_with:category_id|array',
];
}
Array and Nested Validation
Validate arrays and nested data structures:
public function rules(): array
{
return [
// Validate array items
'tags' => 'required|array|min:1|max:10',
'tags.*' => 'string|max:50',
// Validate nested objects
'author' => 'required|array',
'author.name' => 'required|string|max:255',
'author.email' => 'required|email',
'author.bio' => 'nullable|string|max:500',
// Validate array of objects
'comments' => 'nullable|array',
'comments.*.user_id' => 'required|exists:users,id',
'comments.*.content' => 'required|string|max:1000',
'comments.*.rating' => 'nullable|integer|min:1|max:5',
];
}
Warning: Be careful with deeply nested validation. Set reasonable limits to prevent performance issues and potential DoS attacks.
Custom Validation Rules
Create custom validation rules for complex logic:
php artisan make:rule Uppercase
<?php
namespace App\Rules;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule;
class Uppercase implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (strtoupper($value) !== $value) {
$fail('The :attribute must be uppercase.');
}
}
}
Use the custom rule in validation:
use App\Rules\Uppercase;
public function rules(): array
{
return [
'code' => ['required', 'string', new Uppercase],
];
}
Closure-Based Custom Rules
For simple custom validation, use closures directly:
public function rules(): array
{
return [
'username' => [
'required',
'string',
'max:50',
function ($attribute, $value, $fail) {
if (preg_match('/[^a-zA-Z0-9_]/', $value)) {
$fail('The username may only contain letters, numbers, and underscores.');
}
},
],
'discount' => [
'required',
'numeric',
function ($attribute, $value, $fail) {
if ($value > $this->input('price')) {
$fail('The discount cannot be greater than the price.');
}
},
],
];
}
After Validation Hook
Run additional logic after validation passes:
use Illuminate\Validation\Validator;
public function withValidator(Validator $validator): void
{
$validator->after(function ($validator) {
// Check business logic that requires multiple fields
if ($this->input('discount') > 0 && !$this->input('discount_reason')) {
$validator->errors()->add(
'discount_reason',
'A discount reason is required when applying a discount.'
);
}
// Validate against external service
if (!$this->checkCouponCode($this->input('coupon_code'))) {
$validator->errors()->add(
'coupon_code',
'The coupon code is invalid or expired.'
);
}
});
}
Prepare Input for Validation
Modify input data before validation:
protected function prepareForValidation(): void
{
$this->merge([
// Sanitize and format phone number
'phone' => preg_replace('/[^0-9]/', '', $this->phone),
// Convert string boolean to actual boolean
'is_published' => filter_var($this->is_published, FILTER_VALIDATE_BOOLEAN),
// Set user_id from authenticated user
'user_id' => auth()->id(),
// Generate slug from title
'slug' => $this->slug ?? Str::slug($this->title),
]);
}
Authorization in Form Requests
Implement authorization logic in the authorize() method:
public function authorize(): bool
{
// Only authenticated users can create posts
if (!auth()->check()) {
return false;
}
// Only admin or premium users can set featured flag
if ($this->input('is_featured')) {
return auth()->user()->isAdmin() || auth()->user()->isPremium();
}
return true;
}
Note: When authorize() returns false, Laravel returns a 403 Forbidden response.
Manual Validation
Sometimes you need to validate manually:
use Illuminate\Support\Facades\Validator;
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
], [
'title.required' => 'Custom error message',
]);
if ($validator->fails()) {
return response()->json([
'success' => false,
'errors' => $validator->errors()
], 422);
}
// Process valid data
$post = Post::create($validator->validated());
return response()->json([
'success' => true,
'data' => $post
], 201);
}
Validating File Uploads
Validate uploaded files in API requests:
public function rules(): array
{
return [
'avatar' => 'required|file|image|max:2048', // Max 2MB
'document' => 'required|file|mimes:pdf,doc,docx|max:10240', // Max 10MB
'images' => 'required|array|max:5',
'images.*' => 'image|mimes:jpeg,png,jpg,gif|max:5120',
];
}
Exercise:
- Create a Form Request for registering new users with validation for name, email, password, and age
- Add custom error messages for all validation rules
- Create a custom validation rule that checks if a username contains profanity
- Implement conditional validation where phone number is required if email is not provided
- Add an
after validation hook that checks if a coupon code is valid by querying the database
Best Practices
- Use Form Requests: Keep controllers thin by moving validation to Form Request classes
- Fail Early: Validate input before performing expensive operations
- Be Specific: Use precise validation rules rather than relying on generic ones
- Sanitize Input: Clean and format data in
prepareForValidation()
- Limit Arrays: Set
max limits on array inputs to prevent abuse
- Clear Messages: Provide user-friendly, actionable error messages
- Document Validation: Include validation rules in your API documentation
Summary
Request validation is essential for building secure, reliable APIs. Laravel's validation system provides powerful tools for ensuring data integrity while maintaining clean, maintainable code. In the next lesson, we'll explore API authentication with Laravel Sanctum.