
Laravel has become the default choice for many PHP developers. Its expressive syntax, powerful ecosystem, and batteries-included philosophy make building modern web applications feel almost effortless. Almost.
Under the surface, many teams repeatedly fall into the same traps: performance bottlenecks, security gaps, confusing codebases, and deployment headaches. The good news is that the most common mistakes in Laravel are predictable and fixable—often with a few targeted changes.
This guide walks through the most frequent Laravel mistakes developers make and how to fix them with clear, practical examples. Whether you are starting with Laravel or maintaining a large production application, these patterns (and anti-patterns) will help you write cleaner, safer, and more scalable code.
1. Overusing Queries and Ignoring Eloquent Performance
Laravel’s Eloquent ORM is powerful and expressive. It also makes it dangerously easy to write inefficient database queries without noticing—especially in loops and relationships. One of the most widespread Laravel mistakes is N+1 queries and unnecessary round-trips to the database.
1.1 The N+1 Query Problem in Laravel
Imagine you want to list posts and the user who wrote each one:
// Controller
$posts = Post::all();
foreach ($posts as $post) {
echo $post->user->name;
}
This looks innocent, but under the hood Eloquent runs:
- 1 query to fetch all posts
- 1 additional query per post to fetch its user
With 100 posts, that means 101 queries instead of 2. At scale, this destroys performance.
1.2 How to Fix N+1 Queries with Eager Loading
Use with() to eager-load relationships:
// Controller
$posts = Post::with('user')->get();
foreach ($posts as $post) {
echo $post->user->name;
}
Now Laravel runs only two queries:
- 1 query to get all posts
- 1 query to get all associated users
For more complex relationships, you can eager-load nested relations:
$orders = Order::with(['customer', 'items.product'])->get();
DB::enableQueryLog() or the built-in debug bar in local) to quickly spot N+1 problems.
1.3 Avoiding Heavy Logic Inside Loops
Combining Eloquent with loops can be dangerous when you call queries repeatedly:
foreach ($users as $user) {
$orders = Order::where('user_id', $user->id)->get();
// ...
}
Fix it by restructuring the query or using relationships and aggregation:
$users = User::with('orders')->get();
foreach ($users as $user) {
$orders = $user->orders;
// ...
}
1.4 Caching Heavy or Repeated Queries
If your application repeatedly runs the same expensive query (for example, a homepage widget or dashboard metrics), combine Eloquent with Laravel’s cache layer:
use Illuminate\Support\Facades\Cache;
$stats = Cache::remember('dashboard.stats', 600, function () {
return [
'users' => User::count(),
'orders_today' => Order::whereDate('created_at', today())->count(),
];
});
This avoids hitting the database on every request and is one of the easiest performance wins in Laravel.
2. Misusing Validation and Letting Bad Data In
Another common Laravel mistake is skipping or misplacing validation logic. When validation is inconsistent or mixed throughout controllers, it becomes hard to maintain and opens the door to security and integrity problems.
2.1 Validating Directly in Controllers (and Why It Hurts)
Many beginners start like this:
public function store(Request $request)
{
$request->validate([
'title' => 'required',
'body' => 'required|min:10',
]);
Post::create($request->all());
}
This works, but as the project grows:
- Validation rules become duplicated across methods and controllers.
- Validation logic mixes with business and persistence logic.
- Reusing validation (e.g. in an API and a web form) becomes painful.
2.2 The Better Approach: Form Request Classes
Extract validation into dedicated Form Request classes:
php artisan make:request StorePostRequest
Then define rules and authorization:
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return auth()->check();
}
public function rules(): array
{
return [
'title' => 'required|string|max:255',
'body' => 'required|string|min:10',
];
}
}
And use it in your controller:
public function store(StorePostRequest $request)
{
Post::create($request->validated());
return redirect()->route('posts.index');
}
This keeps controllers slim and validation reusable and testable.
2.3 Forgetting to Validate API Inputs
Laravel makes it easy to build JSON APIs, but many developers validate only web forms and forget to validate JSON payloads coming from external clients or mobile apps.
The fix: Use the FormRequest pattern consistently for both web and API controllers. Laravel will automatically return JSON validation errors for API requests.
3. Mixing Business Logic Inside Controllers and Models
Laravel’s magic can tempt you to place everything in controllers or Eloquent models: queries, validation, domain rules, third-party calls, and more. This “fat controller” and “fat model” anti-pattern is one of the biggest maintainability mistakes teams make.
3.1 The Fat Controller Anti-Pattern
Consider this controller method:
public function subscribe(Request $request)
{
$request->validate([
'email' => 'required|email',
]);
$user = User::where('email', $request->email)->first();
if (! $user) {
$user = User::create([
'email' => $request->email,
]);
}
// Call external API
$response = Http::post('https://newsletter.example/subscribe', [
'email' => $user->email,
]);
if (! $response->successful()) {
// ... complex error handling
}
// More logic...
}
This method is doing way too much, making it hard to test and reuse. It also tightly couples your controllers to external services.
3.2 Extracting Services and Actions
A more maintainable approach is to extract domain logic into dedicated service or action classes:
class SubscribeUserToNewsletter
{
public function handle(string $email): void
{
$user = User::firstOrCreate(['email' => $email]);
$response = Http::post('https://newsletter.example/subscribe', [
'email' => $user->email,
]);
if (! $response->successful()) {
// handle failure, maybe throw a domain exception
}
}
}
Then, your controller stays lean:
public function subscribe(SubscribeRequest $request, SubscribeUserToNewsletter $action)
{
$action->handle($request->email);
return back()->with('status', 'Subscribed');
}
This pattern is easier to test and reuse in console commands, queued jobs, or event listeners.
3.3 Keeping Models Focused
Eloquent models should primarily handle:
- Database interaction (queries, relationships, scopes)
- Attribute casting and accessors/mutators
- Simple domain rules closely related to persistence
If you find yourself writing long methods in models that:
- Call external APIs
- Contain complex multi-step workflows
- Depend on multiple other models or services
…it is usually time to move that logic into a dedicated service, domain, or action class.
4. Ignoring Laravel Security Best Practices
Laravel gives you a secure foundation by default: CSRF protection, password hashing, guards, and more. But a handful of misconfigurations or shortcuts can quickly undo these safeguards. Security mistakes in Laravel can be subtle yet critical.
4.1 Disabling CSRF Protection
One of the most dangerous habits is disabling CSRF protection because a form or API endpoint “doesn’t work”:
// App\Http\Middleware\VerifyCsrfToken.php
protected $except = [
'*', // <-- extremely dangerous
];
This makes your application vulnerable to cross-site request forgery attacks.
Instead, only exclude truly stateless JSON APIs that are protected in other ways (like tokens) and always add @csrf to Blade forms:
<form method="POST" action="/profile">
@csrf
</form>
4.2 Using Plain Text or Weak Passwords
Laravel’s authentication scaffolding uses bcrypt by default. Problems arise when developers implement custom auth or import users incorrectly:
- Storing raw or MD5-hashed passwords.
- Comparing passwords manually instead of using the
Hashfacade.
Correct approach:
use Illuminate\Support\Facades\Hash;
// Creating users
User::create([
'email' => $email,
'password' => Hash::make($password),
]);
// Verifying passwords
if (Hash::check($plainPassword, $user->password)) {
// Password is valid
}
4.3 Exposing Sensitive Configuration
A classic Laravel security mistake is leaving APP_DEBUG=true in production. This exposes detailed stack traces and environment variables to visitors whenever an error occurs.
Always set in production:
APP_ENV=production
APP_DEBUG=false
Additionally:
- Never commit your
.envfile to version control. - Use environment variables for credentials (DB, mail, third-party APIs).
- Rotate and restrict access to these values.
4.4 Mass Assignment Vulnerabilities
Eloquent’s mass assignment is convenient, but if you are not careful, users can modify fields they should not control.
// Dangerous if unchecked
User::create($request->all());
Fix it with $fillable or $guarded in your models:
class User extends Model
{
protected $fillable = [
'name',
'email',
'password',
];
}
Or explicitly guard sensitive attributes:
protected $guarded = ['is_admin'];
5. Misconfiguring Routes and Middleware
Routing in Laravel is flexible and expressive, but a few missteps can cause subtle bugs, security issues, or performance problems.
5.1 Putting All Routes in web.php
Small projects often start with everything in routes/web.php. Over time, you end up with hundreds of routes, mixing public pages, admin sections, and APIs.
Instead, leverage Laravel’s route files and grouping:
routes/web.phpfor browser-based pages.routes/api.phpfor stateless API endpoints.routes/channels.phpfor broadcasting.
Within those files, group related routes by prefix and middleware:
Route::middleware(['auth', 'verified'])
->prefix('admin')
->name('admin.')
->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index'])
->name('dashboard');
});
5.2 Forgetting to Protect Sensitive Routes
A surprisingly frequent Laravel error is forgetting to wrap administrative or internal routes with the right middleware:
// Missing auth middleware
Route::get('/admin/users', [AdminUserController::class, 'index']);
Fix it with auth, roles, or policies:
Route::middleware('auth')->group(function () {
Route::get('/admin/users', [AdminUserController::class, 'index'])
->middleware('can:viewAny,App\\Models\\User');
});
5.3 Not Using Route Model Binding
Manually looking up models in every route handler is repetitive and error-prone:
public function show($id)
{
$post = Post::findOrFail($id);
// ...
}
Laravel’s route model binding removes boilerplate and automatically 404s when necessary:
// routes/web.php
Route::get('/posts/{post}', [PostController::class, 'show']);
// Controller
public function show(Post $post)
{
// $post is already resolved
}
You can also customize keys (like slugs) in your models using getRouteKeyName().
6. Poor Use of Migrations and Database Structure
Laravel migrations make schema changes repeatable and safe—if you use them correctly. Mismanaging them can lead to production downtime, broken deployments, and data loss.
6.1 Editing Old Migrations in Existing Projects
A very common mistake is editing existing migration files after they have already been run in shared environments:
- Changing column names or types in an old migration.
- Adding new columns to an already-applied migration.
This breaks the assumption that migrations represent a linear history of your database.
Correct approach: never change old migrations in production applications. Instead, create a new migration that modifies the existing table:
php artisan make:migration add_status_to_orders_table --table=orders
6.2 Forgetting Indexes on Frequent Queries
Eloquent makes queries simple, but the database still needs proper indexing. If your app slows down, it might be because columns used in where, join, or order by clauses lack indexes.
In your migrations, add indexes deliberately:
Schema::table('orders', function (Blueprint $table) {
$table->index('user_id');
$table->index(['status', 'created_at']);
});
6.3 Running Heavy Migrations Without Downtime Planning
Large production databases cannot always be altered safely with a simple php artisan migrate. Operations like adding indexes on huge tables or changing column types can lock tables.
Solutions include:
- Using separate maintenance windows for heavy migrations.
- Breaking migrations into smaller, reversible steps.
- Using database-specific tools (e.g., pt-online-schema-change for MySQL).
7. Underusing Artisan, Queues, and Events
Laravel offers a rich ecosystem of tools to structure and scale your app. Many teams overlook queues, events, and Artisan commands, and instead cram everything into web requests.
7.1 Doing Heavy Work Synchronously
Sending emails, resizing images, generating reports, and hitting third-party APIs inside a web request slows down the user experience and can cause timeouts.
Instead, push heavy jobs to queues:
php artisan make:job SendWelcomeEmail
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public User $user) {}
public function handle(): void
{
Mail::to($this->user->email)->send(new WelcomeMail($this->user));
}
}
Dispatch the job from controllers or listeners:
SendWelcomeEmail::dispatch($user);
7.2 Ignoring Events and Listeners
Events decouple core actions from side effects. If your controller both creates an order and sends multiple notifications, it becomes hard to maintain.
Introduce events like OrderPlaced and listeners like SendOrderConfirmation or NotifyWarehouse. This keeps your domain logic clean and extendable.
7.3 Not Leveraging Artisan Commands
Instead of manually running complex one-off scripts, encapsulate them in Artisan commands:
php artisan make:command RecalculateUserStats
Artisan commands are versioned, testable, and easily scheduled with Laravel’s scheduler.
8. Misunderstanding Configuration, Environments, and Caching
Laravel’s configuration system is powerful but often misunderstood. Incorrect use of environment variables and caching can lead to hard-to-debug behavior between local and production.
8.1 Using env() Directly in Your Code
A subtle but critical Laravel mistake is using env() outside of configuration files, like in controllers or services:
$apiKey = env('THIRD_PARTY_API_KEY'); // anti-pattern
When you run php artisan config:cache, environment variables are loaded once into config. Further calls to env() might behave unexpectedly.
Correct pattern: reference configuration values instead:
// config/services.php
'newsletter' => [
'api_key' => env('NEWSLETTER_API_KEY'),
],
// Usage in code
$apiKey = config('services.newsletter.api_key');
8.2 Forgetting to Cache Configuration and Routes in Production
Laravel can speed up production significantly using config and route caching:
php artisan config:cachephp artisan route:cachephp artisan view:cache
Add these commands to your deployment pipeline to ensure each release is optimized.
8.3 Forgetting to Clear Caches After Changes
Another frequent Laravel bug arises when developers change configuration or routes but forget to clear caches, leading to confusing discrepancies between code and behavior.
Use these commands to reset as needed:
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan cache:clear
9. Overcomplicating Blade Views and Frontend Structure
Blade is simple and powerful, but without discipline, views quickly transform into messy templates full of logic and duplication.
9.1 Embedding Complex Logic in Views
Blade views should focus on presentation. If you find SQL queries, heavy loops, or business decisions inside templates, you are doing too much in the view layer.
Move data preparation to controllers, view composers, or view models. In Blade, keep logic to simple conditionals and loops:
// Controller
$posts = Post::published()->latest()->take(10)->get();
return view('posts.index', compact('posts'));
// Blade
@foreach ($posts as $post)
<h2>{{ $post->title }}</h2>
@endforeach
9.2 Duplicated Layouts and Components
Copy-pasting HTML for headers, footers, and cards across Blade files becomes a maintenance nightmare.
Use Blade layouts and components instead:
<!-- resources/views/layouts/app.blade.php -->
<html>
<head>@yield('title')</head>
<body>
@include('partials.nav')
<main>@yield('content')</main>
</body>
</html>
<!-- In a view -->
@extends('layouts.app')
@section('content')
<h2>Posts</h2>
@endsection
9.3 Not Escaping Output Properly
Laravel’s Blade escapes output by default using {{ }}, which protects against XSS. Problems arise when developers use {!! !!} to output raw HTML without sanitizing content.
Limit raw output to trusted, sanitized content only. For all user-generated data, always use the escaped syntax.
10. Skipping Tests and Relying on Manual QA
Laravel ships with first-class testing support for unit, feature, and browser tests. Yet many teams still rely mostly on manual testing, making refactors risky and bug-prone.
10.1 Not Writing Tests for Critical Flows
Start by covering your most important flows:
- User registration and login.
- Payments and order flows.
- APIs consumed by external clients.
Example feature test:
public function test_user_can_register()
{
$response = $this->post('/register', [
'name' => 'Jane Doe',
'email' => 'jane@example.com',
'password' => 'secret123',
'password_confirmation' => 'secret123',
]);
$response->assertRedirect('/home');
$this->assertDatabaseHas('users', ['email' => 'jane@example.com']);
}
10.2 Ignoring Factories and Seeders
Laravel model factories make it easy to create test data:
User::factory()->count(10)->create();
Combine them with seeders for realistic local environments. This reduces setup time for developers and ensures consistent test data.
10.3 Not Running Tests in CI
Testing only on local machines means bugs frequently slip into production. Instead, run your Laravel test suite as part of your continuous integration pipeline so that failing tests block deployments.
11. Deployment, Logging, and Observability Mistakes
Laravel applications often run flawlessly in local environments, then behave unpredictably in staging or production. Many of these problems trace back to deployment and logging mistakes.
11.1 Deploying Without Optimizing or Clearing Caches
If your deployment script only pulls code and runs composer install, you are likely missing important steps.
A healthier deployment sequence often includes:
- Running database migrations.
- Caching routes, config, and views.
- Clearing old caches and compiled files when needed.
11.2 Ignoring Logs or Logging Too Little
Laravel’s logging system (via Monolog) can write to files, external services, and more. Yet many apps either log nothing meaningful or log far too much noise.
Best practices include:
- Using appropriate log levels (
debug,info,warning,error). - Centralizing logs across servers.
- Logging important domain events and unexpected states.
11.3 Not Monitoring Queues, Jobs, and Scheduled Tasks
Queues and scheduled commands are powerful but add complexity. If they fail silently, your app may appear fine while critical background work stops.
Solutions:
- Supervise queue workers (e.g., with Supervisor or systemd).
- Alert on failed jobs via Laravel’s
failed_jobstable. - Monitor the scheduler and key commands.
12. How to Systematically Avoid Common Laravel Mistakes
Fixing individual Laravel mistakes is helpful, but the real improvement comes from building habits and structures that prevent them from happening in the first place.
- Adopt coding standards: Use tools like PHP_CodeSniffer and static analysis to enforce consistent Laravel patterns.
- Automate checks: Run tests, linters, and security checks on every pull request.
- Document architecture decisions: Clarify where business logic lives (services, actions, models) and how routes, events, and queues are used.
- Review performance regularly: Profile queries, cache hot paths, and track response times.
- Revisit security: Periodically review environment config, authentication flows, and permissions.
Laravel rewards teams that combine its elegant syntax with solid engineering discipline. By understanding the most common mistakes and applying the fixes in this guide, you can keep your application fast, secure, and enjoyable to work with.
Frequently Asked Questions About Common Laravel Mistakes
What are the most common performance mistakes in Laravel?
The most common performance mistakes in Laravel include N+1 database queries caused by missing eager loading, running heavy jobs like email sending or report generation synchronously instead of using queues, not caching configuration, views, or expensive queries, and forgetting proper database indexes. Fixes typically involve using with() for eager loading, dispatching jobs to queues, caching frequently used data, and designing database indexes based on real query patterns.
How can I avoid security vulnerabilities in a Laravel project?
To avoid security vulnerabilities in Laravel, always keep APP_DEBUG=false in production, never commit .env files, use mass assignment protection with $fillable or $guarded, validate all user input with Form Requests, keep CSRF protection enabled for stateful web routes, hash passwords using Laravel’s Hash facade, and escape all user-generated content in Blade views with {{ }}. Regularly review authentication, authorization, and permissions for sensitive routes.
Should business logic go in Laravel controllers or models?
In Laravel, controllers should coordinate requests and responses, while models focus on persistence concerns such as relationships, attribute casting, and query scopes. Complex business logic that spans multiple models or interacts with external systems is usually better placed in dedicated service classes, domain classes, or action objects. This separation keeps controllers and models lean, makes code easier to test, and avoids the “fat controller” and “fat model” anti-patterns.
How should I manage Laravel migrations in long-running projects?
For long-running Laravel projects, avoid editing or deleting old migrations that have already been executed in shared environments. Instead, create new migrations to modify existing tables or columns. Use explicit indexes for frequently queried columns, test migrations on staging databases, and plan downtime or online schema changes for heavy operations. Keep migration history linear and clear so new team members can reliably reproduce the database schema from scratch.
What is the best way to structure a large Laravel application?
Large Laravel applications benefit from clear boundaries: thin controllers, focused models, explicit service or domain layers, events and listeners for side effects, queues for heavy or asynchronous work, and separate route groups for web, admin, and API endpoints. Consistent naming, feature-based folder organization, and comprehensive tests also help. Following these practices minimizes coupling, keeps modules independent, and reduces the risk of common Laravel mistakes as the project grows.
