20+ Laravel best practices, tips and tricks to use in 2023
Hundreds of developers subscribed to my newsletter.
Join them and enjoy free content about the art of crafting websites!
For most Laravel projects, the best practices can be summarized as two points:
- Stick to the defaults;
- Defer as much work as possible to the framework.
Whether you are running Laravel 10, 9 or 8, let’s see in details how I can help you improve any codebase with tons of tips and tricks.
Table of contents
- Keep Laravel up to date
- Keep packages up to date
- Keep your project tested
- Stick to the default folder structure
- Use custom form requests
- Use single action controllers
- Use policies
- Use anonymous migrations (Laravel 8 and above)
- Use the down() method correctly
- Use Eloquent’s naming conventions for table names
- Prevent N+1 issues with eager loading
- Use Eloquent’s strict mode
- Use the new way of declaring accessors and mutators
- Use dispatchAfterResponse() for long-running tasks
- Use queues for even longer running tasks
- Lazily refresh your database
- Make use of factories
- Test against the production stack whenever it’s possible
- Use database transactions
- Don’t waste API calls, use mocks
- Prevent stray HTTP requests
- Don’t track your .env file
- Don’t track your compiled CSS and JavaScript

Keep Laravel up to date
Keeping Laravel up to date provides the following benefits:
- Improved security, because Laravel regularly releases security fixes.
- Better performance: Laravel updates often include performance improvements, such as faster load times and more efficient code.
- New features and functionality. These are way we use and love Laravel and why it changed our lives.
- Compatibility with the latest official and community packages.
If Laravel updates scare you, it’s because your codebase isn’t tested. Keep reading to learn more about automated testing.
Keep packages up to date
Access to thousands of community packages is what makes our job easier.
But the more packages you use, the more vulnerabilities you might be subject to.
Regularly running composer update
goes a long way toward a more secure codebase.
Keep your project tested
Writing automated tests is a vast and lesser-known topic among developers.
But did you know it’s also the only best practice that ensures reliability?
Here are the benefits of a good test suite:
- Fewer bugs;
- Happier customers;
- Happier employers;
- Confident developers. You won’t fear breaking something when coming back to the project after a while;
- New hires can be productive from day one, especially if you follow Laravel’s guidelines;
Ensuring your projects’ reliability with something other than gut instinct will make you an immensely better developer.
Laracasts provides free testing courses to help you get started. One with PHPUnit, the industry standard, and one with Pest, the best testing framework on this planet that modernizes and simplifies testing in PHP.
Stick to the default folder structure
Do you know why you’re using a framework?
- It frames your work with a set of guidelines that you can follow to ensure every member of your team is on the same page;
- It provides many complex, tedious, and battle-tested features for free, so you can focus on coding what is specific to your project.
So, why should you stick to Laravel’s default project structure?
- Convenience. Laravel’s default way of doing things is documented. When you come back on a project weeks or months later, you will thank your past self for this.
- Working with team mates is considerably easier. They know Laravel, just like you. Use this common knowledge to help the project move forward instead of reinventing the wheel every time.
When should you not stick to the defaults?
When the size of your project actually requires to do things differently.
The most popular custom project structure for Laravel is the domain-driven architecture.
Use custom form requests
The main reason to use custom form requests are:
- Reusing validation across multiple controllers;
- Offloading code from bloated controllers;
Creating custom form requests is as simple as running this Artisan command:
php artisan make:request StorePostRequest
Then, in your controller, just type-hint it:
use App\Http\Requests\StorePostRequest; class PostController{ function store(StorePostRequest $request) { $validated = $request->validated(); Post::create($validated); // }}
Custom requests can also be used for authorization, if you feel like Policies are overkill.
Use single action controllers
Sometimes, despite following all the good practices, your controllers become too big.
Laravel provides a way to help you fix this: Single Action Controllers.
Instead of containing multiple actions, like Resource Controllers, Single Action Controllers contain just one.
To create one, use the php artisan make:controller ShowPostController --invokable
command.
This will create a controller with only one action called __invoke
(learn more about the __invoke magic method).
Then, in your routes, you can do this instead:
-use App\Http\Controllers\PostController; +use App\Http\Controllers\ShowPostController; -Route::get('/posts/{post}', [PostController::class, 'show']); +Route::get('/posts/{post}', ShowPostController::class);
This is a subjective best practice and it’s up to you to decide whether you want to use single action controllers or not.
Use policies
Policies exist for a few reasons:
- Reusing authorization logic across multiple controllers;
- Offloading code from bloated controllers;
- Checking out the app/Policies folder for everything related to authorizations is natural for everyone.
Use anonymous migrations (Laravel 8 and above)
Anonymous migrations are a great way to avoid class names conflicts.
Laravel generates anonymous migrations for you as long as you’re using Laravel 9 and above:
php artisan make:migration CreatePostsTable
This is how they look:
<?php use IlluminateSupportFacadesSchema;use IlluminateDatabaseSchemaBlueprint;use IlluminateDatabaseMigrationsMigration; return new class extends Migration { …}
But did you know you can also use them with Laravel 8? Just replace the class names, and you’ll be good to go.
Use the down()
method correctly
The down()
(used by the php artisan migrate:rollback
command) is ran when you need to rollback changes you made to your database.
Some people use it, some don’t.
If you belong to the people who use it, you should make sure your down()
method is implemented correctly.
Basically, the down()
method must do the opposite of the up()
method.
use IlluminateSupportFacadesSchema;use IlluminateDatabaseSchemaBlueprint;use IlluminateDatabaseMigrationsMigration; return new class extends Migration { public function up() { Schema::table('posts', function (Blueprint $table) { // The column was a boolean, but we want to switch to a datetime. $table->datetime('is_published')->nullable()->change(); }); } public function down() { Schema::table('posts', function (Blueprint $table) { // When rolling back, we have to restore the column to its previous state. $table->boolean('is_published')->default(false)->change(); }); }}
Use Eloquent’s naming conventions for table names
Laravel’s naming conventions for tables is easy and one best practice that will simplify your team’s life.
First, let me remind you that the framework does it all for you when you’re using Artisan commands like php artisan make:model Post --migration --factory
.
For whatever reason, if you can’t use those commands, here’s an overview:
- For a
Post
model, name your tableposts
. Basically use the plural form (comments
forComment
,replies
forReply
, etc.); - For a pivot table linking a
Post
to aComment
(e.g.comment_post
):- Use both names
- Singular form
- Alphabetic order
Read the documentation for more information.
Prevent N+1 issues with eager loading
I’ve talked about so many good practices, but it’s far to be over!
Ever heard about N+1 problems? Eager loading is a great solution to avoid them.
Let’s say you are displaying a list of 30 posts with their author:
- Eloquent will make one query for those 30 posts;
- Then, 30 queries for each author, because the user relationship is lazily loaded (meaning it’s loaded each time you call
$post->user
in your code).
The fix is simple: use the with()
method, and you’ll go from 31 queries to only 2.
Post::with('author')->get();
To ensure you don’t have N+1 problems, you can trigger exceptions whenever you lazy load any relationship. This restriction should be applied to your local environement only.
Model::preventLazyLoading( ! app()->isProduction());
Use Eloquent’s strict mode
Eloquent’s strict mode is a blessing for debugging. It will throw exceptions when:
- Lazy loading relationships;
- Assigning non-fillable attributes;
- Accessing attributes that don’t exist (or weren’t retrieved).
Add this code in the boot()
method of your AppServiceProvider.php:
Model::shouldBeStrict( ! app()->isProduction() // Only outside of production.);
Use the new way of declaring accessors and mutators
The new way of declarating accessors and mutators was introduced in Laravel 9.
This is how you should declare them now:
use IlluminateDatabaseEloquentCastsAttribute; class Pokemon{ function name() : Attribute { $locale = app()->getLocale(); return Attribute::make( get: fn ($value) => $value[$locale], set: fn ($value) => [$locale => $value], ); }}
You can even cache expensive to compute values:
use IlluminateDatabaseEloquentCastsAttribute; function someAttribute() : Attribute{ return Attribute::make( fn () => /* Do something. */ )->shouldCache();}
The old way looks like this:
class Pokemon{ function getNameAttribute() : string { $locale = app()->getLocale(); return $this->attributes['name'][$locale]; } function setNameAttribute($value) : string { $locale = app()->getLocale(); return $this->attributes['name'][$locale] = $value; }}
Use dispatchAfterResponse()
for long-running tasks
Let’s use the most straightforward example possible: you have a contact form. Sending an email may take between one or two seconds, depending on your method.
What if you could delay this until the user receives your server’s response?
That’s precisely what dispatchAfterResponse()
does:
SendContactEmail::dispatchAfterResponse($input);
Or, if you prefer to dispatch jobs using anonymous functions:
dispatch(function () { // Do something.})->afterResponse();
Use queues for even longer running tasks
Imagine you have to process images uploaded by your users.
If you process every one of them as soon as they’re submitted, this will happen:
- Your server will burn;
- Your users will have to wait in front of a loading screen.
This isn’t good UX, and we can change that.
Laravel has a queue system that will run all those tasks sequentially or with a limited amount of parallelism.
And, to easily manage your jobs through a user interface, Laravel Horizon is what you should use.
Lazily refresh your database
When you can get away with fake data in your local environment, the best thing to do is to test against a fresh database every time you run a test.
You can use the Illuminate\Foundation\Testing\LazilyRefreshDatabase
trait in your tests/TestCase.php.
There’s also a RefreshDatabase
trait, but the lazy one is more efficient, as migrations for unused tables won’t be ran during testing.
Make use of factories
Factories make testing way more manageable. You can create one using the php artisan make:factory PostFactory
command.
Using them, you can create all the resources you need when writing tests.
public function test_it_shows_a_given_post(){ $post = Post::factory()->create(); $this ->get(route('posts.show', $post)) ->assertOk();}
Test against the production stack whenever it’s possible
When running your web application in production, you probably use something other than SQLite, like MySQL.
Then, why are you running your tests using SQLite?
Use MySQL instead. You will be able to catch MySQL-related bugs in your local environement instead of waiting for things to go wrong in production.
Same for caching. If you are running Redis in production, why are you testing using the array driver?
I’m convinced reliability and accuracy are more important than speed in this context.
Use database transactions
In one of my projects, I need to create a database filled with real data provided by CSV files on GitHub.
It takes time and I can’t refresh my database before every test.
So when my tests alter the data, I want to rollback the changes. You can do so by using the Illuminate\Foundation\Testing\DatabaseTransactions
trait in your tests/TestCase.php.
Don’t waste API calls, use mocks
In Laravel, mocks can be used to avoid wasting API calls while testing and being hit with rate limit errors.
Let’s say we are working on a project using Twitter’s API.
In our container, we have a Client
class used to call it.
While running our test suite, we want to avoid unecessary calls to the real thing and the best way to do it is to swap our client in the container by a mock.
$mock = $this->mock(Client::class); $mock ->shouldReceive('getTweet') ->with('Some tweet ID') ->andReturn([ 'data' => [ 'author_id' => '2244994945', 'created_at' => '2022-12-11T10:00:55.000Z', 'id' => '1228393702244134912', 'edit_history_tweet_ids' => ['1228393702244134912'], 'text' => 'This is a tweet', ], ]);
Learn all about mocking on Laravel’s documentation.
Prevent stray HTTP requests
If you want to make sure that all HTTP requests made during your tests are fake, you can use the preventStrayRequests()
method.
It will cause an exception to be thrown if any HTTP requests that do not have a corresponding fake response is executed.
You can use this method in an individual test or for your entire test suite.
Http::preventStrayRequests();
Don’t track your .env file
This best practice may be obvious for certain people, but I think it’s worth mentionning anyway.
Your .env file contains sensitive informations.
Please, don’t track it!
Make sure it’s included in your .gitignore.
And if at some point in the history you tracked it, make sure nobody having access to the Git history can use the credentials that were in it. Change them!
Don’t track your compiled CSS and JavaScript
Your CSS and JavaScript are generated using originals in resources/css and resource/js.
When deploying into production, you either compile them on the server or you create an artifact before.
Especially for people still using Laravel Mix, I recommend to stop tracking them.
It’s quite annoying that every time you change something, a new public/css/app.css or public/js/app.js is generated and need to be commited.
It only takes two lines in your .gitignore to stop this:
public/csspublic/js