I’ve been building apps with Laravel for a few years now — and honestly, it’s been quite a ride.
Laravel makes development fun, but over time I’ve realized something important: the way we write our code matters just as much as what we build.
When I look back at my older projects, some of them are… well, let’s just say “creative chaos.”
Everything worked, but maintaining them? A nightmare.
So in this post, I want to share some lessons I’ve learned about writing clean, scalable, and maintainable Laravel code — based on real experience, not theory.
🧹 Keep Your Controllers Thin
If I could go back in time and tell my beginner self one thing, it would be this:
“Controllers are not the place to dump all your logic.”
When I first started, I put everything in controllers — queries, validations, data processing — basically turning them into giant spaghetti functions.
Now, I separate my concerns:
- Controllers only handle HTTP requests and responses.
- Services handle business logic.
- Repositories handle data access.
Here’s a quick example of how I structure it now:
// App\Http\Controllers\UserController.php
public function store(UserRequest $request, UserService $service)
{
$user = $service->create($request->validated());
return response()->json($user, 201);
}
// App\Services\UserService.php
public function create(array $data)
{
return User::create($data);
}
It keeps things modular, testable, and much easier to read.
🧠 Use Form Requests for Validation
I used to validate directly in controllers — not wrong, but it gets messy fast. Then I discovered Form Requests, and now I can’t imagine coding without them.
Instead of this:
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users',
]);
You can simply create:
php artisan make:request UserRequest
Then define your rules cleanly:
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'unique:users'],
];
}
Now your controller stays neat, and your validation logic lives where it belongs.
⚙️ Dependency Injection Everywhere
Laravel’s dependency injection is one of its best features — but many devs don’t use it enough.
Instead of calling services or repositories with new everywhere, let Laravel handle it for you.
It automatically resolves dependencies via the container.
Example:
public function __construct(private UserService $service) {}
public function index()
{
return $this->service->getAll();
}
Not only is it cleaner, but it’s also easier to test and maintain. You can mock dependencies later without breaking your code.
🗂️ Use Config and Env Files Properly
This one might sound obvious, but I’ve seen too many projects hardcoding stuff like API keys or feature flags.
If you ever see something like this:
$apiKey = '12345-SECRET-KEY';
please stop. 😅
Instead:
- Put it in
.env→THIRD_PARTY_API_KEY=12345-SECRET-KEY - Access it through
config()→config('services.third_party.key')
It’s safer, scalable, and makes your app ready for any environment.
🧩 Keep Business Logic Out of Models
Eloquent models are great, but don’t turn them into “God classes.”
They should know how to talk to the database — not how your entire app works.
If you have complex logic like “calculate user’s rank” or “send bonus email after purchase,” that belongs in a Service Class, not in the model.
A model should be like a data gateway — simple and focused.
🧱 Use DTOs (Data Transfer Objects)
This is a little more advanced, but in 2025, it’s becoming a must for clean architecture.
DTOs (Data Transfer Objects) help you move data between layers without passing raw arrays everywhere. They’re great for clarity and type safety.
Example using spatie/laravel-data:
class UserData extends Data
{
public function __construct(
public string $name,
public string $email,
) {}
}
Then you can use:
$userData = UserData::from($request);
$service->createUser($userData);
You always know what data is being passed — no more mystery arrays.
🧰 Automate with Artisan Commands
If you find yourself repeating the same logic or maintenance tasks, create an Artisan command for it.
For example:
php artisan make:command CleanUpTempFiles
Then automate cleanup, reporting, or daily jobs. It keeps your app lean and your workflow efficient.
🔁 Testing is Not Optional
I used to think testing was “for big companies.” But once my own side projects started growing, I realized how much time it saves me.
At the very least, write Feature tests for your main flows:
php artisan make:test UserRegistrationTest
And use factories and seeders to quickly set up data. You’ll thank yourself later when a refactor doesn’t break everything.
📦 Organize Your Folders
By default, Laravel gives you /app/Http, /Models, /Services, /Jobs, etc.
But feel free to organize them by domain instead of type, like this:
app/
├── User/
│ ├── Controllers/
│ ├── Requests/
│ ├── Services/
│ └── Models/
└── Order/
├── Controllers/
├── Requests/
└── Services/
It makes your project structure easier to navigate, especially as it grows.
🧭 Final Thoughts
I know — some of these practices might feel like “extra work,” but the truth is: clean code pays you back later.
When your project grows, you’ll be grateful you took the time to organize, separate logic, and follow patterns. You’ll ship faster, debug less, and actually enjoy working on your codebase again.
Laravel gives us all the tools for writing beautiful code — we just need to use them wisely.
“Good developers write code that works. Great developers write code that lasts.”
Next up: I’m planning to write about Building Modular Laravel Apps for Better Scalability. Stay tuned — it’s going to be fun!