Why your Laravel controllers should be almost empty
Learn architectural patterns to keep Laravel controllers thin, improve testability, and build maintainable applications.
The Problem with Fat Controllers
Bloated controllers create cascading problems throughout your application:
- Testing Nightmares: Complex controllers are difficult to unit test without mocking half the application
- Reduced Reusability: Business logic trapped in controllers can’t be used from other contexts (jobs, commands, events)
- Poor Readability: New team members can’t quickly understand what a request handler actually does
- Slow Onboarding: Complex controllers require extensive code walkthroughs to understand the flow
The Thin Controller Pattern
The solution is to delegate responsibilities to specialized classes. Each layer has one job and does it well.
Form Requests for Validation
Move validation logic out of controllers into dedicated Form Request classes:
class CreateUserRequest extends FormRequest
{
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8|confirmed',
];
}
}
Your controller then receives pre-validated data—no need to clutter the action with validation rules.
Service Classes for Business Logic
Extract business logic into service layers:
class CreateUserService
{
public function __construct(private UserRepository $repository) {}
public function execute(array $data): User
{
$user = $this->repository->create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
event(new UserCreated($user));
return $user;
}
}
Now your business logic is:
- Testable in isolation
- Reusable from commands, jobs, and scheduled tasks
- Clear about dependencies and side effects
Repositories for Data Access
Isolate database queries behind a consistent interface:
interface UserRepository
{
public function create(array $data): User;
public function find(int $id): ?User;
public function update(User $user, array $data): User;
public function delete(User $user): bool;
}
This makes switching databases or testing strategies straightforward.
Resources for Response Formatting
Use API Resources to handle response formatting:
class UserResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at,
];
}
}
Responses are consistent, testable, and don’t bleed business logic.
Action Classes to Avoid Fat Services
Even services can grow too large. Use single-purpose Action classes to keep logic focused:
class SendUserWelcomeEmail
{
public function __invoke(User $user): void
{
Mail::to($user->email)->send(new WelcomeEmail($user));
}
}
Each action has one responsibility. This prevents the “God Service” anti-pattern where a single service class handles too many concerns.
The Clean Controller
With these patterns in place, your controller becomes beautifully simple:
class UserController extends Controller
{
public function store(
CreateUserRequest $request,
CreateUserService $service,
) {
$user = $service->execute($request->validated());
return UserResource::make($user);
}
}
The entire flow is visible at a glance. There are no hidden dependencies or side effects. Testing is straightforward.
Benefits This Delivers
- Easier Testing: Test business logic without mocking the HTTP layer
- Fewer Merge Conflicts: When multiple developers work on the same feature, logic is distributed across files
- Better Readability: New team members understand the request flow immediately
- Improved Reusability: Business logic can be invoked from anywhere—commands, jobs, webhooks, queues
- Long-Term Maintainability: Thin controllers are easy to modify years later
Conclusion
Your controller’s job is to orchestrate—to connect the HTTP request to your application’s business logic and format the response. Everything else should live in dedicated, testable classes.
Keep controllers thin, and your codebase will thank you.
You might also like
Building modular systems in Laravel - A practical guide
Learn how modular architecture transforms Laravel apps into scalable, maintainable systems.
PHPAn educational look into the Tempest PHP framework
Discover how Tempest, a modern PHP framework, uses code discovery and the latest PHP features to simplify app development and boost productivity.
LaravelAn open, modern approach to HR & PeopleOps
Discover how modern companies are rethinking HR to attract, retain, and empower top developer talent.