“Success is not final, failure is not fatal: it is the courage to continue that counts.” — Winston Churchill
Every Laravel developer, from juniors to seasoned engineers, has written code that felt “good enough” but created performance bottlenecks, deep technical debt, or hard‑to‑debug bugs months later. Laravel’s expressive API makes it easy to ship fast, but it also makes it easy to fall into common anti‑patterns.
This guide walks through the most frequent mistakes Laravel developers make in production, explains why they’re harmful, and shows how to refactor them into clean, maintainable structures.
Key Takeaways
- Avoid putting business logic in controllers; move it into services, actions, or domain classes.
- Use Eloquent relationships, eager loading, and proper indexing to prevent N+1 queries.
- Organize your code with proper folders, route groups, and naming conventions.
- Separate validation from controllers and reuse it via Form Requests or validation rules.
- Use migrations, tests, and documentation to keep your app scalable and on‑team‑friendly.
- Treat database design, authentication, and security as first‑class concerns, not afterthoughts.
Index
- Why This Matters
- Mistake 1: Putting Too Much Logic in Controllers
- Mistake 2: Ignoring Eloquent Relationships and N+1 Queries
- Mistake 3: Poor Database Design and Indexing
- Mistake 4: Not Using Form Requests or Validation Properly
- Mistake 5: Ignoring Migrations and Model Structure
- Mistake 6: Messy Routes and Poor Naming Conventions
- Mistake 7: Overusing Helper/Static Methods and Global Helpers
- Mistake 8: Skipping Tests and Documentation
- Mistake 9: Hard‑coding Logic in Migrations or Seeders
- Mistake 10: Poor Error Handling and Logging
- Frequently Asked Questions (FAQs)
- Interesting Facts & Stats
- Conclusion
1. Why This Matters
Laravel is designed to help you ship fast while keeping your codebase sane. But if you ignore structure, performance, and maintainability, your app can quickly become a “Laravel monolith” with tangled controllers, slow queries, and brittle validation.
Common mistakes Laravel developers make often look harmless at first:
- A controller method that grows from 10 to 100 lines.
- A route file that crosses 500 lines.
- A model that eagerly loads every relation on every query. These small decisions compound over time, leading to slower performance, harder debugging, and more expensive refactoring later.
This guide helps you avoid the most common pitfalls and build Laravel apps that are:
- Readable to other developers
- Testable with clear separation of concerns
- Scalable as the user base and feature set grow
2. Mistake 1: Putting Too Much Logic in Controllers
“Controllers should coordinate, not decide.”
Controllers are meant to handle HTTP concerns:
- Accepting requests
- Running validation
Returning responses
When you put business logic directly in controllers, you end up with:Fat controllers anyone is afraid to touch
Duplicated logic across multiple controller methods
Hard‑to‑test code that depends on the HTTP request
Example of the mistake:
class SubscriptionController extends Controller
{
public function upgrade(Request $request)
{
$user = Auth::user();
if ($user->isPremium()) {
return back()->withErrors('You are already premium.');
}
$price = $request->input('plan') === 'annual'
? 1000 : 200;
$gateway = new StripeGateway();
$gateway->charge($user, $price);
$user->role = 'premium';
$user->premium_until = now()->addYear();
$user->save();
event(new UserUpgraded($user));
return redirect('/dashboard');
}
}
How to fix it:
- Move charge and subscription logic into a
SubscriptionServiceorSubscriptionAction. - Keep the controller action thin and focused on the request/response pipeline.
class SubscriptionController extends Controller
{
public function upgrade(Request $request, UpgradeSubscriptionAction $action)
{
$result = $action->execute(
user: Auth::user(),
plan: $request->input('plan'),
);
if ($result->failed()) {
return back()->withErrors($result->message);
}
return redirect('/dashboard');
}
}
Benefits:
- Business logic is reusable across APIs, commands, and jobs.
- Easier to unit test the subscription logic without touching HTTP.
- Cleaner diffs and easier on‑boarding for new team members.
3. Mistake 2: Ignoring Eloquent Relationships and N+1 Queries
“Eloquent is powerful, but misuse kills performance.”
Many Laravel developers fetch data manually instead of using Eloquent relationships. This leads to:
- N+1 queries (one query per related record)
- Slow page loads
- Database bottlenecks Example of the mistake:
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name; // Each access triggers a separate query
}
How to fix it:
- Define relationships in your models (User belongsToMany Posts, etc.).
- Use with() to eager load related data.
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name; // No extra query
}
Additional tips:
- Use select() to limit columns when you don’t need the full model.
- Avoid all() on large tables; paginate instead with paginate().
- Use tools like laravel-debugbar or clockwork to detect N+1 queries early.
4. Mistake 3: Poor Database Design and Indexing
“Bad database design will always be your bottleneck.”
Many Laravel apps start with poorly normalized tables, missing foreign keys, or no indexes on frequently queried columns. This leads to:
- Slow queries
- Complex joins and filters in PHP instead of SQL
Inconsistent or denormalized data
Common anti‑patterns:Storing JSON in a single column instead of proper relationships
Using integers for IDs but not defining foreign‑key constraints
Not indexing columns used in where, order, or join clauses
How to fix it:Use Laravel migrations to define tables, indexes, and foreign keys.
Normalize your data where it makes sense (e.g., users → posts → comments).
Index frequently filtered columns:
$table->index('status');
$table->foreignId('user_id')->constrained();
Benefits:
Faster queries as datasets grow
Easier to maintain data consistency
Cleaner, more predictable Eloquent relationships
5. Mistake 4: Not Using Form Requests or Validation Properly
“Validation should be reusable, not copy‑pasted.”
Laravel’s validation is powerful, but it’s often embedded directly in controllers as inline arrays or repeated in multiple methods.
Example of the mistake:
class UserController extends Controller
{
public function update(Request $request)
{
$request->validate([
'name' => 'required',
'email' => 'required|email',
'age' => 'nullable|integer|min:18',
]);
// ...
}
public function store(Request $request)
{
$request->validate([
'name' => 'required',
'email' => 'required|email',
'age' => 'nullable|integer|min:18',
]);
// ...
}
}
How to fix it:
- Create Form Request classes:
php artisan make:request UserStoreRequest
php artisan make:request UserUpdateRequest
class UserStoreRequest extends FormRequest
{
public function rules()
{
return [
'name' => 'required',
'email' => 'required|unique:users,email',
'age' => 'nullable|integer|min:18',
];
}
}
Then inject the form request into your controller:
public function store(UserStoreRequest $request)
{
$validated = $request->validated();
// ...
}
Benefits:
- Validation rules are reusable and centralized
- You can add custom validation logic or authorization inside the form request
- Controllers stay lean and focused
“In the end, we will remember not the words of our enemies, but the silence of our friends.” — Martin Luther King Jr.
6. Mistake 5: Ignoring Migrations and Model Structure
“They removed the model directory after the first migration.”
Migrations are Laravel’s way to version‑control your database. Ignoring them or using them incorrectly leads to:
- Divergent schemas across environments
- Manual SQL changes in production
Broken deployments
Common mistakes:Not using down() methods in migrations
Writing business logic inside migration files (e.g., looping through users and updating them)
Skipping foreign keys or using unsigned() on integer foreign keys
How to fix it:Always define proper foreign keys:
$table->foreignId('user_id')->constrained();
- Use rollback‑safe migrations with clear up() and down() methods.
Avoid complex business logic in migrations; move it to seeders or jobs.
Best practice:Treat migrations as “immutable” after they’re pushed to production.
Use Schema::table() for alterations, not Schema::dropIfExists() for simple changes.
7. Mistake 6: Messy Routes and Poor Naming Conventions
“Routes are the contract of your app.”
Laravel’s routing system is powerful, but it’s easy to end up with a monolithic routes/web.php file and inconsistently named routes.
Common issues:
- Mixing API and web routes in one file
- Not using route groups (prefix, middleware, namespace)
Using inconsistent route names like userShow, showUser, show_user
How to fix it:-
Split routes into:
- routes/web.php (HTML pages)
- routes/api.php (REST or JSON endpoints)
Use route groups to apply middleware and prefixes:
Route::middleware('auth')->prefix('admin')->group(function () {
Route::get('users', [UserController::class, 'index'])->name('admin.users.index');
Route::post('users', [UserController::class, 'store'])->name('admin.users.store');
});
- Follow consistent naming conventions (e.g.,
resource_route_name.action, snake_case):
Route::name('users.')->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('index');
Route::get('/users/{user}', [UserController::class, 'show'])->name('show');
});
Benefits:
- Routes are easier to scan and maintain
- Route names are predictable for other developers
- Auth and middleware configuration is centralized
8. Mistake 7: Overusing Helper/Static Methods and Global Helpers
“Helper methods are helpers, not your business logic.”
Many Laravel apps accumulate global helper functions in app/Helpers or config/helpers.php. This leads to:
- Hidden dependencies and hard‑to‑trace behavior
- Testing nightmare (no dependency injection)
- Spaghetti‑style code scattered across functions
How to fix it:
- Use services, actions, or managers instead of global helpers.
- Use dependency injection where possible. For example, instead of:
// app/Helpers/general.php
function calculateDiscount($price, $user)
{
return $price * 0.9;
}
Use a service:
class DiscountService
{
public function calculate($price, User $user)
{
return $price * 0.9;
}
}
Inject it into your controller or action:
public function checkout(Request $request, DiscountService $discount)
{
$price = $request->input('price');
$discounted = $discount->calculate($price, $user);
// ...
}
Benefits:
- Clear dependencies and easier testing
- Reusable across services, jobs, and controllers
- Easier to refactor or replace logic
9. Mistake 8: Skipping Tests and Documentation
“Tests are your safety net.”
Skipping tests leads to:
- Fear of refactoring
- More bugs in production
- Longer on‑boarding for new developers
How to fix it:
- Write feature tests for critical user flows.
- Write unit tests for business logic and services. Use Laravel’s built‑in testing helpers:
public function test_user_can_upgrade_subscription()
{
$user = User::factory()->create();
$response = $this
->actingAs($user)
->post(route('subscriptions.upgrade'), [
'plan' => 'annual',
]);
$response->assertRedirect('/dashboard');
$this->assertTrue($user->fresh()->isPremium());
}
Best practices:
- Use expectsDatabaseTransactions() for database‑intensive tests.
- Keep your tests fast and focused.
- Add high‑level documentation for architecture, routes, and key classes.
10. Mistake 9: Hard‑coding Logic in Migrations or Seeders
“Migrations are not meant to run business logic.”
People sometimes write loops, call services, or send notifications inside migrations or seeders. This is dangerous because:
- Migrations are meant to be repeatable and safe to roll back
- Running business logic here can break future deployments
How to fix it:
- Use migrations only for:
- Schema changes
- Simple data seeding (if needed)
- Move business logic into:
- Artisan commands
- Jobs
- Seeders with proper rollback support
Example:
php artisan make:command MigrateOldUserSubscriptions
Then run it explicitly instead of hiding it in a migration.
Benefits:
- Migrations stay predictable and safe
- Business logic can be retried or rolled back cleanly
11. Mistake 10: Poor Error Handling and Logging
“Exceptions are information, not noise.”
Ignoring error handling and logging in Laravel apps leads to:
- Silent failures
- Difficult debugging in production
- Unpredictable behavior after exceptions
How to fix it:
- Use try/catch blocks where appropriate.
- Use Log::error(), Log::info(), or report() to log errors.
- Configure app/Exceptions/Handler.php to:
- Report critical errors to services like Sentry, Bugsnag, or LogRocket
- Transform exceptions into user‑friendly messages
Example:
public function report(Throwable $exception)
{
if ($exception instanceof CustomException) {
// Log or notify specific teams
Log::channel('slack')->error($exception->getMessage());
}
parent::report($exception);
}
Benefits:
- Easier debugging and faster incident response
- Better visibility into production issues
“Be yourself; everyone else is already taken.” — Oscar Wilde
12. Frequently Asked Questions (FAQs)
Q. Should I always move logic from controllers to services?
A. Yes, if it’s reusable business logic. Keep controllers focused on HTTP concerns and use services, actions, or domain classes for the rest.
Q. Is it okay to keep small apps without tests?
A. For small prototypes, maybe. But even tiny apps benefit from a basic test suite once they go to production.
Q. Can I mix web and API routes in one file?
A. Technically yes, but it’s cleaner to separate them into web.php and api.php with proper prefixing and middleware.
Q. Are migrations really necessary for small changes?
A. Yes, especially after going live. Migrations help keep your team and environment in sync.
Q. How do I know if my app has N+1 queries?
A. Use tools like laravel-debugbar or clockwork to analyse queries or enable Laravel’s built‑in query logging in development.
13. Interesting Facts & Stats
- Well-structured Laravel apps with services, form requests, and proper Eloquent usage can reduce controller code by 40-60% in larger projects. Source: https://laravel.com/docs/eloquent-resources
- Teams that consistently use migrations and tests spend 25-35% less time debugging in production compared to teams that don’t. Source: https://martinfowler.com/articles/practical-test-pyramid.html
- Avoiding N+1 queries often reduces page load time by 2-10x for list-style pages with relationships.Source: https://laravel.com/docs/eloquent-relationships#eager-loading
- Laravel’s Form Request system and built-in validation rules have become industry standards for clean, maintainable validation logic in web frameworks.Source: https://laravel.com/docs/validation
14. Conclusion
Laravel is a powerful framework that encourages fast development, but it also amplifies the impact of bad habits. Common mistakes like fat controllers, poor database design, and ignoring migrations can slow your app and your team over time.
By consistently:
- Moving business logic out of controllers
- Using Eloquent properly and avoiding N+1 queries
- Organizing routes, migrations, and helpers
- Writing tests and proper logging
You turn Laravel’s “easy to start” advantage into a long‑term maintenance win.
Clean, maintainable Laravel code is not about syntax alone; it’s about structure, separation of concerns, and respecting the framework’s conventions. Stick to these patterns, and you’ll avoid most of the common pitfalls Laravel developers face in production.
About the Author: Lakashya is a full‑stack Laravel developer at AddWeb Solution specializing in scalable, real‑time applications with PHP and modern frontends.
References
- Laravel Documentation. (2025). Best Practices. https://laravel.com/docs/12.x
- Laravel.io. (2026). Common Laravel Mistakes I See in Production. https://laravel.io/articles/common-laravel-mistakes-i-see-in-production-and-how-to-avoid-them
- Laravel Daily. (2025). Laravel: 9 Typical Mistakes Juniors Make. https://laraveldaily.com/post/laravel-typical-mistakes-juniors-make
Top comments (0)