Stop writing fat controllers. Learn how to identify controller bloat, extract logic into dedicated classes, and use dependency injection for cleaner code.
Welcome to the first step in our journey to build professional-grade Laravel applications. We are starting our project board application today, and the most important habit you can form is protecting your controllers from becoming "junk drawers."
In this course, we will focus on building a robust, testable, and scalable architecture. While Laravel interfaces and service contracts for cleaner architecture will be our goal later, we start here by mastering the basics of separation of concerns.
A controller's primary responsibility is to handle the HTTP request, coordinate with the application layer, and return a response. When you find yourself writing database queries, calculating business logic, or sending emails directly inside a controller method, you have "controller bloat."
Signs of bloat include:
DB::table() or complex Eloquent queries inside the controller.Let's look at a "bloated" example. Imagine we are creating a project in our project board.
PHPpublic function store(Request $request) { $request->validate(['name' => 'required']); #6A9955">// Logic bloat: calculating limits and setting defaults $count = Project::where('user_id', auth()->id())->count(); if ($count >= 5) { return response()->json(['error' => 'Limit reached'], 403); } $project = Project::create([ 'name' => $request->name, 'user_id' => auth()->id(), 'slug' => Str::slug($request->name), ]); return response()->json($project, 201); }
This logic belongs in a service class. By moving it, the controller becomes a thin "traffic cop." We will create a ProjectCreator class to handle this.
PHPnamespace App\Actions; use App\Models\Project; use Illuminate\Support\Str; class ProjectCreator { public function execute(array $data, int $userId): Project { if (Project::where('user_id', $userId)->count() >= 5) { throw new \Exception("Limit reached"); } return Project::create([ 'name' => $data['name'], 'user_id' => $userId, 'slug' => Str::slug($data['name']), ]); } }
Now, we inject this class into our controller. Laravel’s Service Container automatically resolves the class, making our controller clean and testable.
PHPpublic function store(Request $request, ProjectCreator $creator) { $request->validate(['name' => 'required']); try { $project = $creator->execute($request->all(), auth()->id()); return response()->json($project, 201); } catch (\Exception $e) { return response()->json(['error' => $e->getMessage()], 403); } }
This approach mirrors the principles discussed in Laravel Contextual Binding: Injecting Different Implementations Easily, where we treat components as interchangeable services rather than hardcoded dependencies.
app/Actions.TaskCreator.TaskCreator into your TaskController.store method becomes.User::find($id). Keep the architecture proportional to the complexity.app() helper inside your service classes. Always inject dependencies through the constructor to keep them visible and testable.We've moved from writing procedural code in controllers to an architecture that separates concerns. By extracting logic into dedicated classes and using dependency injection, we ensure our project board remains maintainable as it grows. You now have a blueprint for "thin" controllers that will serve you throughout this course.
Up next: We'll dive deeper into structuring these classes by Implementing the Service Layer to standardize how our application handles complex business processes.
Learn the repository pattern to decouple your Laravel business logic from Eloquent. Master interfaces, concrete implementations, and dependency injection.
Read moreLearn how to implement a service layer in Laravel to encapsulate business logic, reduce controller bloat, and build a more maintainable, testable application.
Project Board Domain Modeling
Advanced Eloquent Scopes and Accessors
Service-Oriented Task Management
REST API Fundamentals with Sanctum
Resource Controllers and API Responses
Handling API Validation and Form Requests
Implementing Middleware for API Security
Database Transactions for Data Integrity
Error Handling and Global Exceptions
Introduction to Laravel Events and Listeners
Asynchronous Processing with Queues
Job Chaining and Batching
Feature Testing Fundamentals
Mocking Services and Repositories in Tests
Testing Events and Jobs
Database Factories and Seeding
API Versioning Strategies
Advanced Request Filtering and Sorting
Handling File Uploads in REST APIs
Real-time Notifications with Broadcasting
Using Observers for Model Lifecycle Hooks
Implementing Policies for Authorization
Customizing Authentication Guards
Rate Limiting API Endpoints
Eloquent Performance Optimization
Caching Strategies for Performance
Using Traits for Code Reuse
Advanced Dependency Injection with Service Providers
Command Line Tools with Artisan
Scheduled Tasks and Cron Jobs
Integrating Third-Party Services
Handling Webhooks
Logging and Monitoring
Database Migrations Best Practices
Advanced Testing: Integration Tests
Testing API Authentication
Code Quality and Static Analysis
Project Structure for Large Applications
Environment and Configuration Management
Deploying Laravel Applications
Database Indexing Strategies
Using Value Objects
Strategy Pattern for Business Rules
Advanced Queue Monitoring
Building a Search API
Handling Concurrency and Race Conditions
API Documentation with OpenAPI
Testing with Test Doubles
Implementing Multi-Tenancy
Refactoring Legacy Code
Using Middleware for Feature Flags
Building Reusable Packages
Performance Profiling
Secure API Design
Event Sourcing Concepts