Laravel best practices fall into six categories: project structure, controller logic, API design, database queries, caching, and testing. This guide covers 19 actionable practices with code examples to help you write cleaner, more maintainable Laravel applications and ship with confidence.
Each practice is something you can apply to your current project today. Whether you're starting fresh or refactoring an existing codebase, these patterns keep your Laravel apps scalable, testable, and easy for your team to work with.
Before diving into the details, here's the full list at a glance. Use this as a quick reference, or jump to any practice that's relevant to what you're working on right now. Each one is covered in detail with code examples below.
| # | Practice | Category | Priority | What it helps you avoid |
|---|---|---|---|---|
| 1 | Use the most stable release | General | High | Security vulnerabilities, missed performance gains |
| 2 | Keep business logic in service classes | Architecture | High | Tightly coupled controllers, duplicated logic |
| 3 | Follow controller best practices | Controllers | High | Bloated controllers, untestable code |
| 4 | Use helper functions | Code quality | Medium | Reinventing built-in functionality |
| 5 | Obey the single responsibility principle | Architecture | High | Methods that do too much, hard-to-trace bugs |
| 6 | Use the Artisan CLI tool | Tooling | Medium | Manual scaffolding, slower workflow |
| 7 | Validate in request classes | Validation | High | Inline validation clutter, duplicated rules |
| 8 | Timeout HTTP requests | Reliability | Medium | Indefinitely hanging requests |
| 9 | Design APIs for longevity | API design | High | Breaking changes, inconsistent responses |
| 10 | Use mass assignments | Database | Medium | Verbose model creation, unprotected fields |
| 11 | Chunk data for heavy tasks | Performance | Medium | Memory exhaustion on large datasets |
| 12 | Implement a caching strategy | Performance | High | Unnecessary database calls, slow response times |
| 13 | Keep .env out of application code | Configuration | High | Broken config caching, missing values |
| 14 | Use Eloquent over raw SQL | Database | Medium | Hard-to-read queries, missed ORM features |
| 15 | Structure your project for maintainability | Architecture | High | Disorganized codebases, content in the wrong layer |
| 16 | Follow naming conventions | Code quality | Medium | Inconsistent codebase, team confusion |
| 17 | Use standard Laravel tools | Tooling | Medium | Unnecessary dependencies, maintenance overhead |
| 18 | Use shorter, readable syntax | Code quality | Low | Verbose code where Laravel provides shortcuts |
| 19 | Implement the down() migration method | Database | Medium | Failed rollbacks, broken deployments |
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.
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.
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);
// ...
}
}
Clean controllers are the foundation of a maintainable Laravel app. When your controllers stay thin and focused, your entire codebase becomes easier to test, debug, and extend.
Three principles to follow:
Use resource controllers for CRUD operations. Laravel's resource controllers give you a standardized set of RESTful routes and methods. Instead of defining each route manually, use php artisan make:controller ProductController --resource to scaffold a controller with index, create, store, show, edit, update, and destroy methods. This keeps your API endpoints predictable and your routing clean.
Inject dependencies instead of instantiating them directly. As shown in practice #2, constructor injection through Laravel's service container makes your controllers more testable and loosely coupled. When you type-hint a dependency in your constructor, Laravel resolves it automatically — no manual instantiation needed.
Keep controller methods short. If a method starts doing too much, move the logic to a service class or action class. A good rule of thumb: if a controller method exceeds 10-15 lines, it's likely handling responsibilities that belong elsewhere.
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.
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'];
}
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.
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.
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(...);
....
}
Strong API design reduces coupling between your frontend and backend and makes your application easier to evolve over time. Three practices that pay off immediately:
Use middleware for authorization and permissions. Applying auth and permission checks at the middleware level gives you centralized control over your API endpoints. This keeps authorization logic out of your controllers and makes it easy to update permissions across multiple routes in one place.
Adopt API versioning from the start. Versioning your endpoints (e.g., /api/v1/products) lets you introduce updates and improvements without breaking existing integrations. This is especially valuable when third parties or mobile apps consume your API.
Use Eloquent API resources for response serialization. Instead of returning raw model data, use Laravel's API resources (php artisan make:resource ProductResource) to control exactly what your API returns. This gives you a consistent, maintainable response format and makes it easy to add or modify fields without touching your controller logic.
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.
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
}
});
A smart caching strategy keeps your app fast under load and reduces unnecessary database queries. Laravel's cache system is flexible and straightforward — the key is using it intentionally rather than as an afterthought.
Cache frequently accessed data that doesn't change often. Data like configuration settings, menu structures, or category lists are great candidates. Caching these reduces database retrieval overhead on every request.
Use cache tags for granular invalidation. Tags let you group related cached items so you can flush an entire group when the underlying data changes, without clearing your entire cache. This keeps your cached data fresh without sacrificing performance.
Leverage the remember() method. This is one of Laravel's most useful caching patterns — it checks whether data exists in cache and only queries the database if it doesn't:
$products = Cache::remember('products', 3600, function () {
return Product::all();
});
This single method handles the check-then-store pattern that would otherwise require several lines of conditional logic.
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.
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.
A well-organized project structure makes your codebase navigable from day one and keeps it that way as your team and application grow. Three principles to follow:
Adhere to the Laravel directory structure. Laravel's default directory layout is designed for a reason — it makes finding and debugging code intuitive for any developer familiar with the framework. Resist the urge to create custom folder hierarchies unless your application genuinely outgrows the defaults.
Break large applications into modules. As your app scales, modularization keeps things manageable. Group related controllers, models, services, and routes into self-contained modules so each piece of your application can evolve independently.
Keep marketing content out of your application layer. Blog posts, landing pages, FAQs, and other marketing content don't belong in migration-managed models or hard-coded templates. A headless CMS handles this cleanly — for example, ButterCMS integrates natively with Laravel and lets your marketing team manage content independently, so your codebase stays focused on application logic.
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:
|
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 |
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.
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')
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');
});
}
}
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:
Following the Laravel best practices outlined in this article gives you a cleaner, more scalable codebase — and a faster development workflow for your entire team. Whether you're structuring your project, designing APIs, or optimizing performance with caching, each practice builds on the others to create applications that are easier to maintain and extend.
If you're looking to add CMS-powered content to your Laravel app without fighting your framework, ButterCMS integrates natively with Laravel and keeps your codebase just as clean as these best practices recommend. Your marketing team gets full control over blog posts, landing pages, and SEO content while your application layer stays focused on what it does best. Start a free trial to see how it fits, or request a demo and we'll walk you through it. You can also check out our guide on building a Laravel blog with ButterCMS to jump straight into a hands-on example.
Laravel is an open-source PHP framework built on the Model-View-Controller (MVC) pattern. It's designed to make common development tasks like routing, authentication, caching, and database management simpler and more expressive, so you can focus on building your application rather than re-solving infrastructure problems.
Laravel addresses many of the pain points PHP developers encounter: it provides built-in authentication and authorization, pre-configured error handling, a task scheduling system, an expressive CLI (Artisan) for scaffolding and automation, out-of-the-box testing tools like Pest and PHPUnit, and a dedicated notification and event system. Its large community also means vulnerabilities are identified and patched quickly.
The highest-impact practices are keeping business logic in service classes (not controllers), validating in FormRequest classes, using Eloquent over raw SQL, following Laravel's naming conventions, and implementing a caching strategy for frequently accessed data. These five practices alone eliminate the most common sources of technical debt in Laravel applications.
Stick to Laravel's default directory structure unless your application genuinely outgrows it. As your app scales, break it into self-contained modules that group related controllers, models, services, and routes. Keep marketing content (blog posts, landing pages, FAQs) out of your application layer entirely. A headless CMS like ButterCMS handles this cleanly through API integration.
Keep controllers thin. Use resource controllers for CRUD operations, inject dependencies through the constructor rather than instantiating them directly, and move any business logic that exceeds 10-15 lines into a service or action class. Your controllers should coordinate, not compute.
API resources (php artisan make:resource) give you a dedicated transformation layer between your Eloquent models and your JSON responses. Instead of returning raw model data, which exposes your database structure and can change unpredictably, API resources let you define exactly which fields to include, how to format them, and how to handle relationships. This makes your API responses consistent, versioned, and maintainable.
Structure your tests using the Arrange-Act-Assert (AAA) pattern for readability. Use the RefreshDatabase trait so each test starts with a clean slate. Write integration tests that verify interactions between different parts of your application—including database operations, API requests, and external service calls. Mock external services like payment gateways so your tests run reliably without depending on third-party availability.