20 Laravel Best Practices for Developers in 2026
-
Last Updated On
If you have worked with Laravel, you might already know its capabilities and popularity. Laravel powers more than half a million live websites across the globe. Developers prefer it because of its flexible architecture and strong community support.
To build a Laravel application, there are numerous options available. But building an app is one thing, and building it the right way is another thing. To develop a functional, efficient app, you need to follow the Laravel best practices.
In this guide, we will explore Laravel development best practices to write clean, reliable code. Whether you’re working on a small project or a large application, these tips will help you build with confidence and avoid common mistakes.
Table of Contents
Before we dive deeper into each of the facets, here’s an everyday checklist for developers. This overview highlights the most important Laravel best practices to keep with you during Laravel development projects.
| # | Recommended Practice | Area | Importance | Helps Prevent |
|---|---|---|---|---|
| 1 | Work with the latest stable Laravel version | General Development | High | Security risks and outdated performance improvements |
| 2 | Move core business logic into service layers | Application Structure | High | Repeated logic and overloaded controllers |
| 3 | Keep controllers clean and focused | Controllers | High | Difficult testing and oversized controller files |
| 4 | Make use of Laravel helper methods | Code Standards | Medium | Writing duplicate utility functions |
| 5 | Follow the single responsibility approach | Architecture | High | Complex methods that become difficult to maintain |
| 6 | Utilize Artisan commands during development | Development Tools | Medium | Time-consuming manual setup tasks |
| 7 | Handle validation through request classes | Validation | High | Repetitive validation rules inside controllers |
| 8 | Define time limits for external requests | Application Stability | Medium | Requests getting stuck without response |
| 9 | Build APIs with future scalability in mind | API Development | High | Version conflicts and inconsistent outputs |
| 10 | Use Laravel’s mass assignment features | Database Handling | Medium | Lengthy model setup and unsafe field assignments |
| 11 | Process large datasets in smaller batches | Performance Optimization | Medium | High memory consumption during heavy operations |
| 12 | Introduce an effective caching mechanism | Performance | High | Repeated database queries and slower loading |
| 13 | Avoid calling .env values directly in app code | Configuration Management | High | Issues with cached configuration values |
| 14 | Prefer Eloquent instead of raw database queries | Database Operations | Medium | Hard-to-maintain and less readable SQL queries |
| 15 | Maintain a clean and scalable project structure | Project Architecture | High | Disorganized files and tightly mixed responsibilities |
| 16 | Stick to consistent naming patterns | Code Consistency | Medium | Confusion across teams and inconsistent codebases |
| 17 | Use Laravel’s built-in ecosystem and packages | Tooling | Medium | Extra maintenance from unnecessary third-party tools |
| 18 | Choose concise and readable Laravel syntax | Code Readability | Low | Overly verbose code where simpler options exist |
| 19 | Always define the down() migration method | Database Management | Medium | Migration rollback failures and deployment issues |
Whether you are optimizing existing applications or building one from scratch, these practices can help you create cleaner, more scalable, and easier-to-maintain Laravel applications.
Building a Laravel app is not only about functionality, it’s about maintaining a clean and organized codebase.
Laravel offers a robust foundation to develop web applications. By following a few proven best practices, you can improve code quality and create applications that are easier to scale and maintain.
One of the most important Laravel best practices is keeping the framework updated. Running the latest supported version helps your application stay secure, stable, and compatible with modern packages and tools. It also gives you access to new features, performance improvements, and a better developer experience.
Many developers delay updates because they worry about breaking the application. In most cases, this happens when the project lacks proper testing or has too many unnecessary custom changes. Regular updates are usually much easier to manage than waiting for years and handling a large upgrade all at once.
A good approach is to make Laravel upgrades a part of regular maintenance. Follow the official upgrade guides whenever a new version is released and make small updates consistently. This keeps your application healthier and reduces future development problems.
One common mistake developers make in Laravel projects is putting too much logic inside controllers. Controllers should mainly handle requests and return responses. When business logic is added directly into them, the code quickly becomes difficult to read, manage, and test.
A better approach is to move complex tasks into service classes. This keeps your controllers clean and focused while making the logic reusable across different parts of the application.
Bad Example:
public function store(Request $request)
{
$order = new Order();
$order->user_id = auth()->id();
$order->total_amount = 0;
$order->status = 'pending';
$order->save();
foreach ($request->items as $item) {
$product = Product::find($item['product_id']);
if ($product->stock < $item['quantity']) {
return back()->with('error', 'Product is out of stock.');
}
$subtotal = $product->price * $item['quantity'];
OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price' => $product->price,
'subtotal' => $subtotal,
]);
$product->decrement('stock', $item['quantity']);
$order->total_amount += $subtotal;
}
$order->save();
return redirect()->route('orders.show', $order);
}
Here, the controller is directly handling the image upload logic, which can make the method harder to maintain as the application grows.
Good Example:
public function store(Request $request, OrderService $orderService)
{
$order = $orderService->createOrder($request->items);
return redirect()->route('orders.show', $order);
}
class OrderService
{
public function createOrder(array $items): Order
{
$order = Order::create([
'user_id' => auth()->id(),
'total_amount' => 0,
'status' => 'pending',
]);
$totalAmount = 0;
foreach ($items as $item) {
$product = Product::findOrFail($item['product_id']);
if ($product->stock < $item['quantity']) {
throw new Exception('Product is out of stock.');
}
$subtotal = $product->price * $item['quantity'];
OrderItem::create([
'order_id' => $order->id,
'product_id' => $product->id,
'quantity' => $item['quantity'],
'price' => $product->price,
'subtotal' => $subtotal,
]);
$product->decrement('stock', $item['quantity']);
$totalAmount += $subtotal;
}
$order->update([
'total_amount' => $totalAmount,
]);
return $order;
}
This example clearly shows that the bad version keeps order creation, stock validation, item calculation, and database updates inside the controller, while the good version moves that business logic into a dedicated service class.
This will keep the controller clean while the service class handles the business logic separately. It improves readability, reusability, and testing.
Clean controllers are very important for an efficient Laravel app. When your controllers are small and focused, the project will become easy to manage and debug.
One important practice is using resource controllers for CRUD operations. Laravel provides a simple way to generate controllers with standard methods like index, store, update, and destroy.
php artisan make:controller ProductController --resource
This command creates a controller with all the common CRUD methods already structured for you.
Another best practice is dependency injection. Instead of manually creating services within controllers, let Laravel inject them automatically.
public function __construct(ProductService $productService)
{
$this->productService = $productService;
}
This keeps the controller loosely connected to other parts of the application and makes testing easier.
It is also important to keep controller methods short. If a method becomes too large, move the extra logic into service classes or action classes.
public function store(ProductRequest $request)
{
$this->productService->createProduct($request->validated());
return redirect()->back();
}
Short and focused controller methods improve readability and help maintain a clean project structure.
The Single Responsibility Principle means that a class or method should handle only one task. This makes the code easier to understand, test, and maintain.
A common mistake is writing large methods that perform multiple operations together.
Bad Example:
public function getOrderSummary()
{
if (
$this->order &&
$this->order->payment_status === 'paid' &&
$this->order->isDelivered()
) {
return [
'order_number' => $this->order->order_number,
'status' => 'completed',
'message' => 'Order has been paid and delivered.',
];
}
return [
'order_number' => $this->order?->order_number,
'status' => 'pending',
'message' => 'Order is not completed yet.',
];
}
This method checks order availability, payment status, delivery status, and prepares response data in one place, making it harder to maintain.
This method handles multiple responsibilities at once, making it difficult to read and maintain.
Better Approach:
public function getOrderSummary(): array
{
return $this->isCompletedOrder()
? $this->getCompletedOrderSummary()
: $this->getPendingOrderSummary();
}
private function isCompletedOrder(): bool
{
return $this->order &&
$this->order->payment_status === 'paid' &&
$this->order->isDelivered();
}
private function getCompletedOrderSummary(): array
{
return [
'order_number' => $this->order->order_number,
'status' => 'completed',
'message' => 'Order has been paid and delivered.',
];
}
private function getPendingOrderSummary(): array
{
return [
'order_number' => $this->order?->order_number,
'status' => 'pending',
'message' => 'Order is not completed yet.',
];
}
This version keeps the same output but separates the condition checking and response preparation into smaller methods. Each method now has one clear responsibility.
Breaking the logic into smaller methods improves readability and keeps each method focused on a single responsibility. This makes future updates and debugging much easier.
Validation is an important part of every Laravel application. It helps protect your application from invalid data and keeps your code more reliable. However, placing validation rules directly inside controllers can quickly make them large and difficult to manage.
A better approach is to move validation logic into dedicated Request classes. This keeps controllers clean and makes validation rules easier to reuse, update, and understand.
Bad Example:
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:255',
'email' => 'required|email|unique:customers,email',
'phone' => 'nullable|string|max:20',
'password' => 'required|string|min:8|confirmed',
]);
Customer::create([
'name' => $validated['name'],
'email' => $validated['email'],
'phone' => $validated['phone'] ?? null,
'password' => Hash::make($validated['password']),
]);
}
Here, the controller is responsible for handling the request, validating customer data, and creating the customer record.
Here, the controller is handling both the request and the validation logic together.
Good Example:
public function store(CustomerRequest $request)
{
Customer::create([
'name' => $request->validated('name'),
'email' => $request->validated('email'),
'phone' => $request->validated('phone'),
'password' => Hash::make($request->validated('password')),
]);
}
class CustomerRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:customers,email',
'phone' => 'nullable|string|max:20',
'password' => 'required|string|min:8|confirmed',
];
}
}
This keeps validation rules inside a dedicated request class, making the controller cleaner and easier to maintain.
This structure keeps validation separate from business logic and makes the application easier to maintain as it grows.
Good API design makes your application easier to maintain and scale over time. It also creates a smoother connection between the frontend and backend systems.
One important practice is using middleware for authorization and permission checks. Instead of writing permission logic inside every controller, middleware allows you to manage access rules from a central place. This keeps controllers cleaner and improves security management.
Another smart practice is API versioning. Creating endpoints like /api/v1/products helps you introduce future updates without breaking existing integrations. This becomes especially useful when mobile apps or third-party systems rely on your API.
You should also use Laravel API resources instead of returning raw database data directly.
php artisan make:resource ProductResource
API resources allow you to control exactly what data is returned in responses. This creates a more consistent and maintainable API structure while keeping sensitive or unnecessary fields hidden.
Well-designed APIs make future development much easier and help applications grow without creating unnecessary complexity.
Caching is one of the easiest ways to improve application performance in Laravel. A good caching strategy reduces unnecessary database queries and helps pages load faster, especially when traffic increases.
You should cache data that does not change frequently, such as menus, settings, category lists, or common queries. This reduces the need to fetch the same information repeatedly from the database.
Laravel also provides cache tags, which allow you to group related cache data together. This makes it easier to clear only the specific cached content when updates occur, rather than removing the entire cache.
The remember() feature provides effortless control over Laravel’s caching patterns.
$products = Cache::remember('products', 3600, function () {
return Product::all();
});
Using an intuitive “check-then-store” method, it first checks for existing data in the cache, and queries the database only if the data is not found.
To maintain consistency with the config caching system, you must define values using env(..) and retrieve them using config(..) in application code.
It is important to secure the API keys when integrating external services. Avoid extracting keys directly from your app code after securing them in your key store (.env). Follow this process:
$aws_access_key = env("AWS_ACCESS_KEY_ID");
It is ideal to apply it to your config/api.php like this:
[
...
'aws_access_key' = env("AWS_ACCESS_KEY_ID"),
]
And when any part of your project requires the key, retrieve it using Laravel’s config function.
$aws_access_key = config('api.aws_access_key');
Manage your environment variable values effortlessly: when a variable is missing from the .env file, simply define a default value. This allows Laravel to cache configuration data from config files faster, improving overall application efficiency.
Your codebase stays easy to navigate, maintain, and scale from the outset with a properly structured project. Follow these 3 pillars to achieve this:
The “skinny, controllers, fat models” approach is one of the most efficient ways to keep your codebase manageable and clean.
Instead of bloating controllers with business logic and complex database operations when using Query Builder or raw SQL, these should be handled within dedicated Repository classes or Eloquent models.
With this approach, the controllers’ only task is to process requests and return responses, while models handle the core business logic behind the scenes. You must define a clear distinction between application logic and request handling for an organized, more cleaner and scalable codebase. Check out two contrasting scenarios:
Avoid:
public function index()
{
$products = Product::where('is_active', true)
->where('stock_quantity', '>', 0)
->whereHas('category', function ($query) {
$query->where('status', 'active');
})
->orderBy('created_at', 'desc')
->get();
return view('products.index', [
'products' => $products,
]);
}
Ideal:
/* controller */
<?php
namespace App\Http\Controllers;
use App\Models\Product;
class ProductController extends Controller
{
public function index()
{
return view('products.index', [
'products' => Product::getAvailableProducts(),
]);
}
}
/* Model */
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
public static function getAvailableProducts()
{
return self::where('is_active', true)
->where('stock_quantity', '>', 0)
->whereHas('category', function ($query) {
$query->where('status', 'active');
})
->latest()
->get();
}
}
Laravel offers a wide range of built-in helper functions and utility methods. Leverage these existing tools to minimize manual effort and code repetition, thereby maximizing overall development efficiency. Instead of writing custom logic or generating random strings, it is advisable to rely on Laravel’s Illuminate\Support\Str tools to manage it effortlessly:
$slug = Str::random(24);
Helpers are available to lend a hand throughout the application, making operations faster and more consistent.
Laravel’s Artisan CLI tool enables the automation of repetitive development tasks and the scaffolding of project setup. You can use Artisan to generate files and configurations with a single command:
php artisan make:model Customer -m
Here are some of the commonly used Artisan commands:
php artisan make:request CustomerRequest
php artisan optimize:clear
php artisan migrate
php artisan test
To streamline your development workflow, explore Laravel’s official documentation for more commands.
Implementing timeouts is a key Laravel best practice. Setting appropriate timeouts ensures your controller sends reliable HTTP requests. By default, Laravel uses a 30-second timeout. Without a timeout, delayed HTTP requests can bottleneck your application, making it unresponsive.
Learn how to execute a 120-second HTTL timeout request:
use Illuminate\Support\Facades\Http;
public function checkShippingRate(StoreRequest $request)
{
$response = Http::connectTimeout(10)
->timeout(60)
->get('https://api.shipping-provider.com/rates', [
'country' => $request->country,
'weight' => $request->weight,
]);
return $response->json();
}
Here:
connectTimeout(10) means Laravel will wait up to 10 seconds to connect to the external API.
timeout(60) means the full HTTP request must complete within 60 seconds.
Laravel offers a secure way to handle model data: Mass assignments. It helps prevent unintended alterations to sensitive data, such as administrative status or passwords.
For example, if you want to save a product in the product model, do not use this code:
$order = new Order;
$order->customer_name = $request->customer_name;
$order->customer_email = $request->customer_email;
$order->shipping_address = $request->shipping_address;
$order->total_amount = $request->total_amount;
$order->save();
You can simply use the model’s create() method and pass the validated request data directly into it:
public function store(OrderRequest $request)
{
Order::create($request->validated());
return redirect()->route('orders.index')
->with('success', 'Order created successfully.');
}
This keeps the controller shorter and ensures only validated, allowed fields are saved.
When you’re working with large datasets, looping through the dataset is preferable to retrieving everything at once. Follow this process:
Do not use this code:
$orders = Order::where('status', 'pending')->get();
foreach ($orders as $order) {
$order->update([
'status' => 'processing',
]);
}
Define the amount of data to be processed and the closure to leverage the chunk approach. For instance:
Order::where('status', 'pending')
->chunk(100, function ($orders) {
foreach ($orders as $order) {
$order->update([
'status' => 'processing',
]);
}
});
A better option when updating records is chunkById():
Order::where('status', 'pending')
->chunkById(100, function ($orders) {
foreach ($orders as $order) {
$order->update([
'status' => 'processing',
]);
}
});
chunkById() is safer when records are being updated during the loop because it processes records based on their primary ID instead of relying only on offset-based pagination.
The connection between models becomes simpler when Eloquent enables you to write easily maintainable, readable code. Utilize Laravel tools like scopes, soft deletes, events, and more to ensure error-free data in your table, with conditions set for your database.
Laravel Eloquent eliminates the need to write complex SQL code. If you need to extract some products and their relevant categories, adding custom filters, you don’t need this type of SQL query:
SELECT *
FROM `orders`
WHERE EXISTS (
SELECT *
FROM `users`
WHERE `orders`.`user_id` = `users`.`id`
AND `users`.`deleted_at` IS NULL
)
AND `payment_status` = 'paid'
AND `order_status` = 'completed'
ORDER BY `created_at` DESC;
Use Eloquent instead:
Order::has('user')
->paid()
->completed()
->latest()
->get();
Model scopes:
class Order extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
public function scopePaid($query)
{
return $query->where('payment_status', 'paid');
}
public function scopeCompleted($query)
{
return $query->where('order_status', 'completed');
}
}
This version is easier to read because the query clearly says: get orders that have a user, are paid, are completed, and show the latest first.
Both code snippets produce the same result: the correct product listings according to the applied filters. The difference lies in the simplicity and clarity of the Eloquent code.
Adhering to the Laravel community’s accepted naming conventions and the PSR standards is critical for file organization. Here’s a guide table to eliminate errors and inconsistencies from your Laravel development workflow:
| What | How | Good | Bad |
|---|---|---|---|
| Model | singular | User | Users |
| hasOne or belongsTo relationship | singular | articleComment | articleComments, article_comment |
| All other relationships | plural | articleComments | articleComment, article_comments |
| Table | plural | article_comments | article_comment, articleComments |
| Route | plural | articles/1 | article/1 |
| Pivot table | singular model names in alphabetical order | article_user | user_article, articles_users |
| Table column | snake_case without model name | meta_title | MetaTitle; article_meta_title |
| Model property | snake_case | $model->created_at | $model->createdAt |
| Controller | singular | ArticleController | ArticlesController |
| Contract (interface) | adjective or noun | AuthenticationInterface | Authenticatable, IAuthentication |
| Trait | adjective | Notifiable | NotificationTrait |
| Foreign key | singular model name with _id suffix | article_id | ArticleId, id_article, articles_id |
| Method | camelCase | getAll | get_all, GetAll |
Leverage a versatile ecosystem of official Laravel tools to streamline and accelerate your application development.
Some widely used Laravel tools include:
Using these tools can minimize development time and effort, instill consistency across your codebase, and offer strong community support for updates.
The down() method in the migration file is one of the most commonly overlooked implementations in Laravel development. To consistently maintain reliable database rollbacks, every modification to the migration file through the up() method should have a corresponding down() implementation.
Here’s a situation where migration adds a new fee column to the orders table: (Migration with only up() method)
return new class extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone_number')->nullable()->after('email');
});
}
// Missing down() method
};
This migration adds a phone_number column, but if you need to rollback the migration, Laravel will not know how to remove that column properly.
down() method implementation blocks the fee column creation:
return new class extends Migration
{
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('phone_number')->nullable()->after('email');
});
}
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('phone_number');
});
}
};
In this example, the up() method adds the phone_number column, and the down() method removes the same column during rollback. This keeps database migrations safe and reversible.
The shorter and clearer your Laravel codebase is, the more consistent the results it produces. For instance, if you want to extract specific session data from a request session, it would be wrong to use:
$request->session()->get('cart_total');
Or
Session::get('cart_total');
You should be using this style of concise, simple code writing instead:
session('cart_total');
A practical controller example:
public function checkout(Request $request)
{
session([
'checkout_step' => 'payment',
'cart_total' => 1500,
]);
$cartTotal = session('cart_total');
return view('checkout.payment', [
'cartTotal' => $cartTotal,
]);
}
This keeps session handling shorter and easier to read while preserving the same functionality.
Unnecessary complications and vague code writing are not welcome in the Laravel framework. The most effective and robust Laravel development projects usually:
From organizing and structuring application logic to optimizing performance with efficient caching and neat APIs, our collection of Laravel best practices helps create a stable, reliable, and manageable development workflow.
Laravel features an ecosystem of foundational tools like Eloquent ORM, migrations, Artisan CLI, built-in security, validation layers, and much more, as you already know from the aforementioned sections. Leveraging these tools enables you to:
If you’re planning to modernize your existing Laravel application or ideating a new project, it is ideal to follow these guidelines for optimal results. Additionally, consider hiring pre-vetted Laravel developers to ensure your project is completed with confidence.
A few of the most vital Laravel best practices involve service classes separation from application logic, maintaining lightweight controllers, validation handling with FormRequest classes, and using Eloquent ORM for neat database operations. Additionally, Laravel’s naming standards and caching implementation improves long-term readability, maintainability, and performance.
Laravel’s default folder structure keeps the entire project organized, helping developers stay familiar with the framework. Related parts like controllers, services, models, and routes can be stacked together into dedicated modules as the application scales. Lastly, I recommend keeping marketing content (blogs, landing pages, etc.) separate from application logic and accessing it through a CMS.
JSON responses can be easily formatted and controlled in a structured way through the Laravel API resources. Rather than exposing raw model data mistakenly, resources enable developers to customize output formats, select defined fields, and maintain relationships more efficiently.
Arrange-Act-Assert pattern is the most ideal for Laravel testing, keeping the structure clear and consistent. Minimize unexpected failures and start each test with a pristine environment through database refresh traits. As a rule of thumb, it is imperative to thoroughly test application workflows – APIs, authentication flows, and database interactions. And external services, such as payment gateways and third-party APIs, must be mock-tested to enable quick, reliable testing.
Utilize API resources for data formatting, validate requests through FormRequest classes, and implement authentication and authorization systems to create a flawless Laravel API ecosystem. Instill long-term scalability and client compatibility by versioning APIs, applying caching where necessary, and handling exceptions neatly.