REST API Development

Request Validation for APIs

22 min Lesson 8 of 35

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:
  1. Create a Form Request for registering new users with validation for name, email, password, and age
  2. Add custom error messages for all validation rules
  3. Create a custom validation rule that checks if a username contains profanity
  4. Implement conditional validation where phone number is required if email is not provided
  5. 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.