Laravel Tutorial
19+ Laravel Best Practices for Developers in 2025
Posted by Maab Saleem on July 24, 2025
Like any mature development framework, Laravel has a set of best practices that help developers write clean, testable, and maintainable code. These best practices cover everything from file structure to database queries, validation, and error handling.
However, since PHP is a very flexible language, and because some of these best practices can feel overwhelming at first, it’s easy for developers, especially those new to Laravel, to take shortcuts or skip them altogether.
This can lead to technical debt, hard-to-maintain code, and a much slower debugging and development process down the line.
The good news is, if you take a bit of time to understand these best practices, applying them in your projects becomes much easier. In this guide, we’ll break down the most recommended Laravel best practices, so you can build better applications with less frustration.
Table of contents
What is Laravel?
Laravel is a popular and respected open-source, free PHP programming framework designed to make development processes more appealing without sacrificing quality. It uses a Model View Controller (MVC) pattern, which makes the language more relatable and adaptable.
PHP problems that Laravel fixes
Laravel solves various PHP problems. For example, it:
-
Ensures unauthorized users have no access to paid or secured resources and configures almost everything out of the box.
-
Pre-configures error handling, and logs errors in an
AppExceptionsHandler
class. You can customize this behavior using thewithExceptions
method inbootstrap/app.php
. -
Has several tools to address PHP’s application development and deployment inefficiencies. These include deployment tools like Laravel Forge and Laravel Vapor, starter kit tools like Laravel Breeze, and many others.
-
Has a built-in command scheduler that allows you to configure your schedule within Laravel, solving the problem of implementing a task scheduling system.
-
Solves the problem of repetitive programming tasks using the PHP Artisan command line to complete numerous programming tasks.
-
Quickly spots and handles technical vulnerabilities due to its large user base, active community, and maintenance team.
-
Includes out-of-the-box testing tools like Pest and PHPUnit and functionalities to enable expressive testing. It also supports executing automated testing sessions that are more precise than manual ones.
-
Provides a dedicated notification and event system instead of handling them solely through PHP.
-
Allows for configuring URL routing for easy route configuration.
Laravel best practices for developers in 2025
This section reviews the essential best practices when working with Laravel, covering Laravel API best practices, Laravel project structure best practices, and Laravel controller best practices. By following these guidelines, you'll be able to write clean, efficient, and maintainable code.
Always use the most stable release
The most recent version of Laravel, 12.x, was released on February 24, 2025. While Laravel 11.x brought major features like opt-in API and broadcast routing, Laravel 12 is more of a maintenance release. It mainly focuses on updating dependencies and improving performance. According to the official docs, no breaking changes were introduced, which should make it much easier for developers to upgrade from 11.x to 12.x.
If you haven’t updated yet, or are running an outdated version of Laravel, now’s a good time to migrate. The longer you delay framework updates, the harder they become later on.
Keep business logic in the service class
To ensure clean code, users must implement abstractions. While controllers typically manage business logic, there are instances where other controllers may need to reuse this logic. In such cases, you should encapsulate this logic within separate service classes. This approach helps maintain code cleanliness by avoiding redundancy and promoting reusability.
Here's an example of a bad approach where the business logic is tightly coupled to the controller and is not reusable:
namespace App\Http\Controllers;
...
class OrderController extends Controller
{
public function placeOrder(Request $request)
{
$data = $request->all();
$user = User::find($data['user_id']);
$order = new Order();
$order->user_id = $user->id;
$order->total = $data['total'] + ( $data['total'] * 0.5);
$order->status = 'pending';
$order->save();
// ...
}
. . .
}
This snippet creates an order record using the user data and the data from the request. It also calculates the total cost of the order.
To do this better, refactor the code to look like this:
namespace App\Services;
...
class OrderService
{
public function createOrder(array $orderData)
{
// Encapsulated business logic
$user = User::find($orderData['user_id']);
$order = new Order();
$order->user_id = $user->id;
$order->total = $data['total'] + ( $data['total'] * 0.5);
$order->status = 'pending';
$order->save();
return $order;
}
}
Then, inject the service into the controller as demonstrated below:
...
use App\Services\OrderService;
class OrderController extends Controller
{
private $orderService;
public function __construct(OrderService $orderService)
{
$this->orderService = $orderService;
}
public function placeOrder(Request $request)
{
$orderData = $request->all();
$order = $this->orderService->createOrder($orderData);
// ...
}
}
Use helper functions
Laravel comes with several helper functions and methods. Instead of reinventing the wheel, use the existing methods; it will keep the codebase concise and prevent repetition. A typical example is when you want to generate a random string. Instead of creating a new function that does that, you can use the Illuminate/Support/Str
by doing the following:
$slug = Str::random(24);
These helpers are available anywhere within the application.
Obey the single responsibility principle (SRP)
A class and method should have just one responsibility. This makes software implementation easier and intercepts any unforeseen side effects that may occur due to future changes.
For instance, instead of having a method that returns the transaction data by performing several operations, as demonstrated by the snippet below:
public function getTransactionAttribute()
{
if ($transaction && ($transaction->type == 'withdrawal') && $transaction->isVerified()) {
return ['reference'=>$this->transaction->reference, 'status'=>'verified'];
} else {
return ['link'=>$this->transaction->paymentLink, 'status'=>'not verified'];
}
}
Improve readability by spreading the actions across methods like this:
public function getTransactionAttribute(): bool
{
return $this->isVerified() ? $this->getReference() : $this->getPaymentLink();
}
public function isVerified(): bool
{
return $this->transaction && ($transaction->type == 'withdrawal') && $this->transaction->isVerified();
}
public function getReference(): string
{
return ['reference'=>$this->transaction->reference, 'status'=>'verified'];
}
public function getPaymentLink(): string
{
return ['link'=>$this->transaction->paymentLink, 'status'=>'not verified'];
}
Use the Artisan CLI tool
The artisan CLI tool has a lot of commands that can help scaffold a setup you want. For instance, instead of manually writing your migration files and creating the model, use the command:
php artisan make:model Branding -m
There are several other artisan commands you can use, which include:
-
The create Request command.
php artisan make:request LoginRequest
-
The optimization command, which you can use to clear cache.
php artisan optimize:clear
-
The command to run migrations
php artisan migrate
-
Also, the command to run the tests
php artisan test
Check out the Laravel documentation for even more details.
Carry out validation in request classes
Validation is crucial when handling user input in your Laravel application to ensure data consistency and prevent errors. Inline validation rules in controller methods can become cumbersome and repetitive. To address this, Laravel provides FormRequest classes that allow you to define validation rules in a centralized and reusable way.
Previously, you might have written validation rules directly in your Controller method like this:
public function store(Request $request)
{
$request->validate([
'slug' => 'required',
'title' => 'required|unique:posts|max:255',
...
]);
}
Instead, create a dedicated FormRequest class using the artisan CLI tool:
php artisan make:request StoreRequest
In this class, define your validation rules:
class StoreRequest extends FormRequest
{
...
public function rules(): array
{
return [
'slug' => 'required',
'title' => 'required|unique:posts|max:255',
...
];
}
}
Then, update your controller method to use the FormRequest
class that you created:
public function store(StoreRequest $request)
{
...
}
By using FormRequest
classes, you can decouple validation logic from your controller and reuse these rules across your application. This approach promotes cleaner code, reduces duplication, and makes maintenance easier.
Timeout HTTP request
One of the best practices Laravel has to offer is utilizing Timeouts. By using the timeout method, you can avoid errors when sending HTTP requests from your controller. Laravel defaults to timeout requests to its server after 30 seconds. If there is a delay by the HTTP request and no error message, your app could remain stuck indefinitely.
Here is an example of how to timeout an HTTP request after 120 seconds.
public function store(StoreRequest $request)
{
....
$response = Http::timeout(120)->get(...);
....
}
Take advantage of mass assignments
In Laravel, mass assignments are useful to avoid scenarios where users might unintentionally alter sensitive data, such as passwords or admin status. For instance, suppose you have a product that you want to save in the Product
model. Instead of using the following code:
$product = new Product;
$product->name = $request->name;
$product->price = $request->price;
$product->save();
You can use the create
static method from the model class and pass in the validated request array like so:
Product::create($request->validated());
This method handles mass assignment.
Chunk data for heavy data tasks
When processing a large amount of data from the database, instead of fetching the data and running a loop through the large data, like this:
$products = Product::all() /* returns thousands of data */
foreach ($products as $product) {
...
}
Use the chunk
method by specifying a fixed amount you want to process at a time and the closure for processing. Here is an example:
Product::chunk(200, function ($products) {
foreach ($products as $product) {
// Perform some action on the product
}
});
Do not use environment variables (.env) directly in your code
In Laravel, use env(..) in config files to define values and config(..)
in application codes to retrieve them. This ensures compatibility with the config caching system.
For example, if you have to use an external service that requires API keys, it must be secure. After you've added this key to your key store (.env file) to secure it, instead of retrieving the keys directly, just like this:
$chat_gpt_key = env("CHAT_GPT_API_KEY");
It's best to add it to your config/api.php using the following:
[
...
'chat_gpt_key' => env("CHAT_GPT_API_KEY"),
]
Then, when you need to use the key within any section of your project, just fetch it using the config
function provided by Laravel.
$chat_gpt_key = config('api.chat_gpt_key');
This gives you more control over the environment variable, allowing you to set a default value even when the environment variable doesn't exist in your key store (.env file). Also, using the config method has performance benefits, as Laravel caches the configurations present in the configuration files.
Use Eloquent instead of Query Builder and raw SQL queries
Eloquent allows you to write readable and maintainable code. It comes with useful tools like soft deletes, events, scopes, etc., and will enable you to set conditions for your database table queries to ensure your table's data stays correct. It simplifies managing relationships between models.
You don't need to write complex SQL code when you have Eloquent in Laravel. For instance, if you want to fetch a list of products and their categories, filter by availability and sort them by the most recent. Instead of using the SQL query like this:
SELECT *
FROM `products`
WHERE EXISTS (SELECT *
FROM `categories`
WHERE `products`.`category_id` = `categories`.`id`
AND `categories`.`deleted_at` IS NULL)
AND `is_available` = '1'
ORDER BY `created_at` DESC
You simply use this:
Product::has('category')->isAvailable()->latest()->get();
Both code snippets are retrieving products that have a related category, are marked as available, and are sorted in descending order by creation time. While the first snippet uses raw SQL, the second uses the Eloquent method chaining to apply these conditions, making the intent of the code more explicit and easier to understand.
Maintain Laravel naming conventions
Follow the PSR Standards as stated here, and also use the naming conventions accepted by the Laravel community, which will help in organizing your files. Using consistent naming conventions is important because inconsistencies can cause confusion, which will eventually lead to errors. Here's a tabular guideline:
Use standard Laravel tools
Laravel offers a range of tools to enhance development, including:
-
Sanctum: Simplifies authentication and authorization
-
Socialite: Streamlines social media authentication
-
Jetstream: Accelerates application scaffolding and provides pre-built UI components
-
Mix: Optimizes asset compilation and management
These tools standardize your code, make it maintainable, and are regularly updated with community support.
Use shorter and more readable syntax in your Laravel code
Using shorter syntax makes your Laravel code readable and consistent and reduces cognitive load. For example, if you want to get the particular session data from the request session, instead of using the following:
$request->session()->get('order')
or
Session::get('order')
You can simply use this:
session('order')
Implement the down() Migration method
Most developers often overlook implementing the down()
method in their migration file. This neglect can have significant consequences, particularly in successfully executing rollbacks. Therefore, it is a highly recommended Laravel best practice to always implement the down()
method for every up()
method in your migration file.
For instance, if you have an orders table migration file that creates a new column, fee
:
return new class extends Migration
{
public function up()
{
Schema::table('orders', function (Blueprint $table) {
$table->unsignedInteger('fee')->default(0);
});
}
...
};
You’ll need to implement the down()
method, which negates the creation of the column fee.
return new class extends Migration
{
public function up()
{
. . .
}
public function down()
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('fee');
});
}
}
Additional Laravel best practices to keep in mind
Apart from the Laravel best practices discussed above, there are additional practices that improve your codebase's readability and maintainability. These practices include:
Laravel API best practices
-
Use middleware to implement authorization and permission for your API endpoints. This gives your codebase centralized control and improves scalability.
-
Adopt API versioning so that you can maintain various versions of your endpoints and introduce updates without interfering with existing integrations.
-
Model serialization should be done using Eloquent's API resources, which comply with Laravel API response best practices. This provides an effective and maintainable approach to managing API response, giving you more control and a reusable solution.
Laravel project structure best practices
-
Adhere to the Laravel Directory Structure. This allows you to find and debug your application easily.
-
Implement modularization by breaking down large applications into smaller, manageable modules to improve maintainability and scalability.
- Avoid placing business logic in controllers or routes. Use service classes or jobs for that.
Laravel controller best practices
-
Use Laravel's resource controllers to define RESTful routes and methods for CRUD operations, promoting clean and predictable API endpoints when necessary.
-
Inject dependencies into your controllers instead of directly instantiating them. This promotes decoupling, making your code more testable and flexible.
- Keep controller methods short. If a method starts doing too much, move the logic to a service or action class.
Laravel cache best practice
-
Employ cache tags and cache invalidation strategies to manage cached data effectively and prevent stale or outdated information.
-
One of Laravel's REST API best practices involves caching regularly accessed data, which doesn't change often. This reduces the database data retrieval overhead.
- Use the
remember()
method to cache data only if it doesn’t already exist in cache.
Laravel testing best practices
-
Implement integration tests to verify interactions between different parts of your application, including database operations, API requests, and external service integrations.
-
Structure your tests using the Arrange-Act-Assert (AAA) pattern. This promotes readability and maintainability.
-
Use the
RefreshDatabase
trait to start each test with a clean slate, especially when testing database interactions. -
Mock external services (e.g. payment gateways or APIs) so your tests don’t rely on external systems.
The future of Laravel
Laravel, one of the most popular PHP frameworks and already on its 12th version, will likely continue advancing and growing for the foreseeable future. As we look beyond version 12, here are some exciting possibilities that might shape the future of Laravel:
-
Deeper embrace of cloud-native technologies such as serverless architecture.
-
Laravel might openly embrace GraphQL, allowing developers to build their GraphQL APIs.
-
Major improvements to existing tools in the Laravel ecosystem, as well as the introduction of new tools to enhance developer experience, and so on…
Final thoughts
Following the Laravel best practices outlined in this article will make your next project much cleaner and easier to maintain. The latest version of Laravel includes some new features that can improve your development experience. You can also integrate your Laravel application with ButterCMS to achieve the best results.
Ready to put these Laravel best practices into action? Check out our guide on building a Laravel blog with ButterCMS to see how you can create a powerful, content-driven application in no time.
ButterCMS is the #1 rated Headless CMS
Related articles
Don’t miss a single post
Get our latest articles, stay updated!
Maab is an experienced software engineer who specializes in explaining technical topics to a wider audience.