Laravel REST API Best Practices: Authentication, Validation, and Performance
Laravel REST API Best Practices: Authentication, Validation, and Performance
Building REST APIs in Laravel is straightforward, but making them production-ready requires attention to authentication, validation, error handling, and performance. Here are the patterns and practices I use in real-world projects.
π Authentication Strategies
1. Sanctum for SPA + Mobile Apps
Laravel Sanctum is perfect for:
- Single-page applications (same domain)
- Mobile applications
- Token-based authentication
// Login endpoint public function login(Request $request) { $credentials = $request->validate([ 'email' => 'required|email', 'password' => 'required', ]);
if (Auth::attempt($credentials)) { $request->session()->regenerate(); return response()->json(['user' => Auth::user()]); }
return response()->json(['message' => 'Invalid credentials'], 401); } ```
2. API Tokens for Third-Party Access
For external integrations, use token-based auth:
```php // Generate token $token = $user->createToken('api-access')->plainTextToken;
// Protect routes Route::middleware('auth:sanctum')->group(function () { Route::get('/user', function (Request $request) { return $request->user(); }); }); ```
3. OAuth2 with Passport (Advanced)
For public APIs with multiple clients, Laravel Passport provides OAuth2:
```bash php artisan passport:install ```
β Request Validation
Use Form Requests
Never validate directly in controllers. Use Form Request classes:
```php // app/Http/Requests/StorePostRequest.php class StorePostRequest extends FormRequest { public function rules(): array { return [ 'title' => 'required|string|max:255', 'body' => 'required|string|min:100', 'category_id' => 'required|exists:categories,id', 'tags' => 'sometimes|array', 'tags.*' => 'exists:tags,id', ]; }
public function messages(): array { return [ 'title.required' => 'A title is required for the post.', 'body.min' => 'Post body must be at least 100 characters.', ]; } }
// In controller public function store(StorePostRequest $request) { // Validation already passed! $post = Post::create($request->validated()); return response()->json($post, 201); } ```
Custom Validation Rules
For complex business logic:
```php // app/Rules/ValidPhoneNumber.php class ValidPhoneNumber implements Rule { public function passes($attribute, $value) { return preg_match('/^\+?[1-9]\d{1,14}$/', $value); }
public function message() { return 'The :attribute must be a valid international phone number.'; } } ```
π¦ API Resource Classes
Transform models consistently using API Resources:
```php // app/Http/Resources/PostResource.php class PostResource extends JsonResource { public function toArray($request): array { return [ 'id' => $this->id, 'title' => $this->title, 'slug' => $this->slug, 'excerpt' => $this->excerpt, 'author' => new UserResource($this->whenLoaded('author')), 'category' => new CategoryResource($this->whenLoaded('category')), 'tags' => TagResource::collection($this->whenLoaded('tags')), 'published_at' => $this->published_at?->toIso8601String(), 'created_at' => $this->created_at->toIso8601String(), ]; } }
// In controller return new PostResource($post); // or for collections return PostResource::collection($posts); ```
π¨ Error Handling
Consistent Error Responses
Create a trait for standardized error responses:
```php // app/Traits/ApiResponder.php trait ApiResponder { protected function success($data, $message = null, $code = 200) { return response()->json([ 'success' => true, 'message' => $message, 'data' => $data, ], $code); }
protected function error($message, $code = 400, $errors = null) { return response()->json([ 'success' => false, 'message' => $message, 'errors' => $errors, ], $code); } } ```
Global Exception Handler
Customize `app/Exceptions/Handler.php`:
```php public function render($request, Throwable $exception) { if ($request->expectsJson()) { if ($exception instanceof ValidationException) { return $this->error( 'Validation failed', 422, $exception->errors() ); }
if ($exception instanceof ModelNotFoundException) { return $this->error('Resource not found', 404); } }
return parent::render($request, $exception); } ```
β‘ Performance Optimization
1. Eager Loading
Always eager load relationships to avoid N+1 queries:
```php // Bad: N+1 problem $posts = Post::all(); foreach ($posts as $post) { echo $post->author->name; // Query for each post! }
// Good: Eager loading $posts = Post::with(['author', 'category', 'tags'])->get(); ```
2. Pagination
Always paginate large datasets:
```php // Simple pagination $posts = Post::paginate(15);
// Cursor pagination (better for APIs) $posts = Post::cursorPaginate(15);
// Custom pagination return PostResource::collection($posts)->response(); ```
3. Caching
Cache expensive queries:
```php $posts = Cache::remember('featured_posts', 3600, function () { return Post::featured()->with('category')->get(); }); ```
4. Database Indexing
Add indexes for frequently queried columns:
```php // Migration $table->index('status'); $table->index('published_at'); $table->index(['status', 'published_at']); // Composite index ```
5. API Response Caching
Use HTTP caching headers:
```php return response()->json($data) ->header('Cache-Control', 'public, max-age=3600') ->header('ETag', md5(json_encode($data))); ```
π Security Best Practices
1. Rate Limiting
Protect your API from abuse:
```php // routes/api.php Route::middleware(['throttle:60,1'])->group(function () { Route::get('/posts', [PostController::class, 'index']); });
// Custom rate limits Route::middleware(['throttle:10,1'])->group(function () { Route::post('/login', [AuthController::class, 'login']); }); ```
2. CORS Configuration
Configure CORS properly in `config/cors.php`:
```php 'allowed_origins' => env('CORS_ALLOWED_ORIGINS', 'https://yourdomain.com'), 'allowed_methods' => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'], 'supports_credentials' => true, ```
3. Input Sanitization
Always sanitize user input:
```php // Use validation rules 'email' => 'required|email|max:255', 'content' => 'required|string|max:5000',
// Sanitize HTML if needed $clean = strip_tags($request->input('content')); ```
π API Versioning
Version your APIs for backward compatibility:
```php // routes/api.php Route::prefix('v1')->group(function () { Route::apiResource('posts', PostController::class); });
Route::prefix('v2')->group(function () { Route::apiResource('posts', V2\PostController::class); }); ```
π§ͺ Testing APIs
Use Laravel's HTTP testing:
```php // tests/Feature/PostApiTest.php public function test_can_create_post() { $user = User::factory()->create();
$response = $this->actingAs($user, 'sanctum') ->postJson('/api/v1/posts', [ 'title' => 'Test Post', 'body' => 'Test body content...', 'category_id' => 1, ]);
$response->assertStatus(201) ->assertJsonStructure([ 'data' => ['id', 'title', 'slug'] ]); } ```
π― Key Takeaways
- Use Form Requests for validation
- API Resources for consistent responses
- Eager load relationships
- Paginate large datasets
- Cache expensive operations
- Rate limit to prevent abuse
- Version your APIs
- Test everything
Conclusion
Building production-ready Laravel APIs requires attention to detail across authentication, validation, error handling, and performance. These practices have helped me build APIs that are secure, fast, and maintainable.