17+ Laravel "Bad Practices" You Should Avoid

People keep asking me about "best practices" in Laravel. The thing is that Laravel allows us to perform things in many different ways, so there are almost no obvious-absolute-best practices. However, there are clear BAD practices. So, in this article, I will list the most common ones.


What is Considered a "Bad Practice"?

This is kind of my personal interpretation, but I define bad practice as a code that can cause real negative impact in the future:

  • Security issues
  • Performance issues
  • Increased bugs possibility

Some impact may be noticed only years later by new developers joining the team. But then, the price to fix those issues is often very high, usually about big refactorings.

First, I will list the really bad practices (in my opinion) that lead to severe impacts like performance or security issues.

In the second section of the article, I will list "not so bad" practices, some of which are debatable, so be active in the comments.

Now, let's dive into the list!


Bad Practice 1. Not Preventing N+1 Query with Eager Loading.

Let's start with the "elephant in the room".

Whenever I hear a problem with Laravel performance, I first check how many SQL queries are executed under the hood of Eloquent sentences.

Here is the classic example from the docs:

use App\Models\Book;
 
$books = Book::all();
 
foreach ($books as $book) {
echo $book->author->name;
}

That's right, for every book, it will run another SQL query to get its author.

You can fight this problem of too many (N+1) SQL queries on THREE levels.

  1. FIND & fix them: with Laravel Debugbar
  2. AVOID them: learn to use Eager Loading
  3. PREVENT them: add preventLazyLoading()

Bad Practice 2. Loading Too Much Data from DB.

This is somewhat related, also causing performance issues, but from another angle.

$posts = Post::with('comments')->get();
 
foreach ($posts as $post) {
echo $post->title . ': ' . $post->comments()->count();
}

See what is wrong here?

Yes, we're loading the full comments relationship with all the columns, although we need only the COUNT.

Instead, it should be this:

$posts = Post::withCount('comments')->get();
 
foreach ($posts as $post) {
echo $post->title . ': ' . $post->comments_count;
}

If you run too many SQL queries, they load your DB server and network which will probably just go slower and slower.

But if you load too much data from Eloquent into PHP variables, it's stored in RAM. And if that reaches the memory limit, your server will just crash and will load this for users in the browser:

Also, in the same example above, we load all the data for the Post Model, although we need only the title. If posts contain long text content with 1000+ or more words, all those kilobytes will be downloaded into memory. Multiply that by amount of users viewing the page, and you may have a big problem.

So, it should be:

$posts = Post::select('title')
->withCount('comments')
->get();
 
foreach ($posts as $post) {
echo $post->title . ': ' . $post->comments_count;
}

You may not feel the impact on smaller tables, but it may be just a general good habit to adopt.

So the general rule of thumb is "only load the data you actually need".

I also discuss these performance problems from above deeply in the premium tutorial Optimizing Laravel Eloquent and DB Speed: All You Need to Know and in the course Better Eloquent Performance.


Bad Practice 3. Chain Eloquent without checking.

Have you ever seen this code in Blade?

{{ $project->user->name }}

So, the Project belongs to a User, and it seems legit.

But guess what happens if the User model doesn't exist? For whatever reason: soft-deleted, typo in the name, etc.

Then you will get this error:

There are 4 ways to fix this relationship chain: here's a tutorial about it.

My favorite is just using PHP null-safe operators:

{{ $project->user?->name }}

But of course, it depends on the situation. However, my point is that it's a generally bad practice to chain without checking intermediate objects.

Here's another example:

$adminEmail = User::where('is_admin', 1)->first()->email;

What if there's no record of first()?

So, I see many developers trust/assume that the record will always exist because it exists for them at the moment.

Not a future-proof code.

So, always check if the expected record exists, and gracefully show errors if it doesn't.


Bad Practice 4. API returning 2xx Code with Errors.

Tired of talking about Eloquent? Let's switch to APIs. Have you seen something like this "horror" in Controller?

public function store()
{
if ($someCondition) {
return response()->json([
'error' => true,
'message' => 'Something happened'
], 200);
}
 
// ...
}

There are actually many things that need to be corrected in this snippet, but I want to emphasize that 200 number.

It's actually a default value, so this code would do the same:

return response()->json([
'error' => true,
'message' => 'Something happened'
]);

My point is that if the API has some error, you need to return the error code to the API client.

Imagine the face of a front-end or mobile developer who tries to call this API and gets no error code, but their app doesn't work. And they don't know why because API seems to return a good result!

It's so frustrating that it deserves a meme:

Unfortunately, I didn't find the original author, just this Reddit post.

In general, when creating APIs, communicate with all parties involved in consuming that API and agree on the standard requests/responses to avoid misunderstandings. Or, if you create a public API, please document all possible return results and their status codes.

There are also slight differences between returning, for example, 401 vs 403 code, and similar examples. But those are not crucial. The most important is the first number: success or not.

In general, there are dozens of HTTP status codes, but usually only about 10 of them are widely used. I like this list explained in human language.


Bad Practice 5. Blindly Trusting User Data Without Validation.

The classic example is...

The full tutorial [17 mins, 3274 words] is only for Premium Members

Login Or Become a Premium Member for $129/year or $29/month
What else you will get:
  • 68 courses (1183 lessons, total 43 h 18 min)
  • 90 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent New Courses