DEV Community

Cover image for Common Mistakes Laravel Developers Make (And How to Avoid Them)

Common Mistakes Laravel Developers Make (And How to Avoid Them)

“Controllers should coordinate, not decide.”

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

  1. Why This Matters
  2. Mistake 1: Putting Too Much Logic in Controllers
  3. Mistake 2: Ignoring Eloquent Relationships and N+1 Queries
  4. Mistake 3: Poor Database Design and Indexing
  5. Mistake 4: Not Using Form Requests or Validation Properly
  6. Mistake 5: Ignoring Migrations and Model Structure
  7. Mistake 6: Messy Routes and Poor Naming Conventions
  8. Mistake 7: Overusing Helper/Static Methods and Global Helpers
  9. Mistake 8: Skipping Tests and Documentation
  10. Mistake 9: Hard‑coding Logic in Migrations or Seeders
  11. Mistake 10: Poor Error Handling and Logging
  12. Frequently Asked Questions (FAQs)
  13. Interesting Facts & Stats
  14. 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 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');
    }
}
Enter fullscreen mode Exit fullscreen mode

How to fix it:

  • Move charge and subscription logic into a SubscriptionService or SubscriptionAction.
  • 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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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',
        ]);

        // ...
    }
}
Enter fullscreen mode Exit fullscreen mode

How to fix it:

  • Create Form Request classes:
php artisan make:request UserStoreRequest
php artisan make:request UserUpdateRequest
Enter fullscreen mode Exit fullscreen mode
class UserStoreRequest extends FormRequest
{
    public function rules()
    {
        return [
            'name' => 'required',
            'email' => 'required|unique:users,email',
            'age' => 'nullable|integer|min:18',
        ];
    }
}
Enter fullscreen mode Exit fullscreen mode

Then inject the form request into your controller:

public function store(UserStoreRequest $request)
{
    $validated = $request->validated();

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Validation rules are reusable and centralized
  • You can add custom validation logic or authorization inside the form request
  • Controllers stay lean and focused

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();
Enter fullscreen mode Exit fullscreen mode
  • 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');
});
Enter fullscreen mode Exit fullscreen mode
  • 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');
});
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

Use a service:

class DiscountService
{
    public function calculate($price, User $user)
    {
        return $price * 0.9;
    }
}
Enter fullscreen mode Exit fullscreen mode

Inject it into your controller or action:

public function checkout(Request $request, DiscountService $discount)
{
    $price = $request->input('price');
    $discounted = $discount->calculate($price, $user);

    // ...
}
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Benefits:

  • Easier debugging and faster incident response
  • Better visibility into production issues

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

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

Top comments (0)