✨ Strapi MCP is now Generally Available - let your agents manage your Strapi content ✨

Ecosystem14 min read

Laravel Best Practices: A Developer's Guide for 2026

January 18, 2025Updated on July 20, 2026
Laravel Best Practices

Laravel is the most-used PHP framework by a wide margin: JetBrains PHP survey puts it at 64% usage among PHP developers, and the laravel/framework package has passed Packagist install count. That popularity means a huge number of Laravel applications in production, and a huge range in code quality between them.

The maintainability gap between a project that's a pleasure to maintain and one that's a nightmare to touch depends on consistent practices. For teams that already know Laravel basics, production work depends on coding standards, security hardening, performance improvement, testing, and maintainability habits that hold up.

Production Laravel work depends on four habits:

  • PSR standards and the "fat models, skinny controllers" pattern help keep Laravel projects readable and maintainable
  • Hypertext Transfer Protocol Secure (HTTPS) enforcement, input validation, role-based access control (RBAC), and rate limiting reduce common security risks
  • Config caching, eager loading, database indexing, and content delivery network (CDN) usage can improve performance without adding much complexity
  • Automated tests and continuous integration and continuous delivery (CI/CD) gates are worth making part of your default workflow

Laravel coding standards

Automate coding standards with Pint and keep the same rules across the team. These practices focus on keeping Laravel projects readable before small inconsistencies turn into review noise or maintenance debt.

Following PSR coding standards

PSR standards bring consistency to your code. The reference points have moved. PSR-2 was officially deprecated in 2019, replaced by PSR-12 standard, which has itself been superseded by the PER Coding Style to keep pace with newer PHP language features. If your style guide still references PSR-2, it's time to update it.

PSR-4 autoloading defines how classes autoload based on file paths and namespaces: sub-namespaces map to subdirectories, and class names must match filenames, including case. Laravel's app directory follows this convention under the App namespace:

{
    "autoload": {
        "psr-4": {
            "App\\": "app/"
        }
    }
}

You don't need to enforce any of this by hand. Laravel Pint ships with every new Laravel application and fixes style automatically; you can run ./vendor/bin/pint to fix all files, or ./vendor/bin/pint --dirty to only touch uncommitted Git changes. Pint supports laravel, per, psr12, and symfony presets, and any PHP CS Fixer rule works in its pint.json config.

Keeping fat models and skinny controllers

Business logic and database queries usually belong in models, while controllers can stay focused on HTTP requests and calling model methods. Controllers packed with query logic become hard to test and harder to reuse. Logic that lives in a model works in controllers and background code such as Artisan commands or queued jobs.

Poor example:

public function index()
{
    $clients = Client::verified()
        ->with(['orders' => function ($q) {
            $q->where('created_at', '>', Carbon::today()->subWeek());
        }])
        ->get();

    return view('index', ['clients' => $clients]);
}

Improved example:

public function index()
{
    return view('index', ['clients' => $this->client->getWithNewOrders()]);
}

class Client extends Model
{
    public function getWithNewOrders()
    {
        return $this->verified()
            ->with(['orders' => function ($q) {
                $q->where('created_at', '>', Carbon::today()->subWeek());
            }])
            ->get();
    }
}

Staying on the latest Laravel version

The current stable release is Laravel 13, which requires PHP 8.3 or higher. Laravel 13 adds a first-party Artificial Intelligence (AI) software development kit (SDK) with a unified Application Programming Interface (API) for text generation and tool-calling agents. It also adds built-in JSON:API resource serialization and native vector query support through methods like whereVectorSimilarTo() with PostgreSQL and pgvector.

Together, those additions make version currency more than a housekeeping task.

Version currency matters more than most teams realize. Laravel's support policy is 18 months of bug fixes and two years of security fixes per release. Laravel 11 stopped receiving security fixes in March 2026, so anything still running it is unsupported software. Laravel 12 receives security fixes only. Regular upgrades also keep you compatible with community packages, where new major versions tend to drop support for EOL framework releases quickly.

Laravel security best practices

Laravel gives you useful security primitives. Production safety comes from applying them consistently. Small gaps in transport security, validation, authorization, rate limiting, or headers tend to show up at the worst time.

HTTPS enforcement in production

In production, enforce HTTPS at the framework level so routes don't serve plaintext by accident. In the boot() method of AppServiceProvider:

if ($this->app->isProduction()) {
    URL::forceHttps();
}

URL::forceHttps() is the current method for Laravel 11 through 13; older tutorials showing forceScheme('https') are out of date. Let's Encrypt provides free Secure Sockets Layer (SSL) certificates if your host doesn't provision them automatically.

Input validation and sanitization

Form Request classes keep validation logic out of controllers and guarantee only the data you intend to accept gets through. You can generate one with php artisan make:request StorePostRequest:

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true;
    }

    public function rules(): array
    {
        return [
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
        ];
    }
}

Type-hint the request in your controller and validation runs automatically; $request->validated() returns only the fields that passed your rules. You can use prepareForValidation() method to sanitize input before rules run. For Cross-Site Request Forgery (CSRF), Laravel handles token generation and verification automatically through the ValidateCsrfToken middleware; your POST forms should include the @csrf Blade directive, which is exactly what the OWASP Laravel Cheat Sheet recommends.

Authentication and role-based access control

Broken access control sits at Open Worldwide Application Security Project (OWASP) access control, so authorization deserves as much attention as authentication. Laravel 12 changed authentication scaffolding: the official release notes state that "With the introduction of these new kits, Laravel Breeze and Laravel Jetstream will no longer receive additional updates." The current starter kits use Laravel Fortify under the hood.

For authorization, Gates and Policies define granular permissions. In Laravel 11 and later, Gates live in AppServiceProvider (not the removed AuthServiceProvider):

Gate::define('update-post', function (User $user, Post $post) {
    return $user->id === $post->user_id;
});

Model-bound policies come from php artisan make:policy PostPolicy --model=Post, and Blade can check permissions with @can('update', $post). A Gate::before callback can grant administrators a global bypass. Apply the same discipline to any external API your app exposes or consumes; Strapi's guides on Representational State Transfer (REST) APIs, including authenticating REST API requests and API security best practices, cover the other side of that boundary.

These checks keep access rules close to the code paths they protect.

Rate limiting and security headers

Rate limiting protects login endpoints and APIs from brute force and abuse. You can define named limiters in AppServiceProvider and attach them with the throttle middleware:

RateLimiter::for('api', function (Request $request) {
    return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});

Content Security Policy headers restrict which external resources can load. That restriction cuts off a whole class of Cross-Site Scripting (XSS) attacks. The Laravel CSP package package supports Laravel 11 through 13:

composer require spatie/laravel-csp
php artisan vendor:publish --tag=csp-config

From there, you can register the middleware globally in bootstrap/app.php:

use Spatie\Csp\AddCspHeaders;

->withMiddleware(function (Middleware $middleware) {
    $middleware->append(AddCspHeaders::class);
})

The OWASP headers guide also recommends X-Content-Type-Options: nosniff and a Strict-Transport-Security header. For multi-factor authentication, Fortify two-factor auth; you can turn it on via Features::twoFactorAuthentication() in config/fortify.php and add the TwoFactorAuthenticatable trait to your User model. If your stack includes a headless CMS, extend the same header and access-control discipline there too; see headless CMS security.

Laravel performance improvements

Most Laravel performance wins come from removing repeated work rather than reaching for exotic infrastructure. Start with framework caches, query behavior, asset delivery, and image size before you add operational complexity.

Configuration and query result caching

At deployment, the framework caching command documentation also lists individual cache commands you can run during deployment:

php artisan config:cache  # caches configuration
php artisan route:cache   # caches routes
php artisan view:cache    # caches views
php artisan event:cache   # caches events

These are the same individual commands for config, routes, views, and events. Once config is cached, the .env file is no longer loaded, so keep env() inside config files, not application code.

For query results, Cache::remember method avoids repeating identical database calls:

$users = Cache::remember('users', 600, function () {
    return DB::table('users')->get();
});

Laravel 12 and 13 also offer Cache::flexible method for stale-while-revalidate behavior, useful when slightly stale data beats a slow response.

Database query improvements

Eager loading fixes the N+1 problem by fetching related data upfront:

$users = User::with('posts')->get();

Better yet, you can make lazy loading impossible to ship by accident. This goes in a service provider's boot() method, and Laravel throws an exception on any lazy load outside production:

Model::preventLazyLoading(! $this->app->isProduction());

For detection tooling, Telescope Query Watcher tags queries slower than a configurable threshold, and the Laravel query detector dev package (Laravel 13 support landed in 2.3.0) surfaces N+1 queries automatically in debug mode.

It's usually better to retrieve only the columns you need instead of User::all():

DB::table('users')->select('name', 'email as user_email')->get();

And frequently queried columns deserve a database index in a migration:

Schema::table('users', function (Blueprint $table) {
    $table->index(['account_id', 'created_at']);
});

Serving static assets from a CDN

Serving compiled assets from a CDN like Cloudflare or Amazon Web Services (AWS) CloudFront moves static files closer to users and takes load off your application server. Default Vite pipeline configuration is one environment variable:

ASSET_URL=https://cdn.example.com

After that, every Vite-compiled asset URL is prefixed with your CDN origin. It produces paths like https://cdn.example.com/build/assets/app.9dce8d17.js. Note that Vite absolute URLs are not rewritten, so image references are safer through Vite::asset() in Blade templates. Cache behavior differs by provider: time to live (TTL) defaults vary.

CloudFront's default TTL is one day, while Cloudflare edge cache TTL for 200 responses is 120 minutes. The same CDN approach applies to API responses from a headless CMS; Strapi's guide to REST API CDN setup walks through that setup.

Image and asset improvements

Two packages cover different jobs here, and picking the right one saves you from bolting on the wrong tool. Intervention Image handles runtime manipulation for thumbnails and responsive variants, including resizing and format conversion. It also covers cropping when you need it. It uses the GD or Imagick extension.

Spatie image compression package does file-level compression on images that are already the right dimensions. It runs them through system binaries like Jpegoptim, Pngquant, and Cwebp. The package includes middleware that compresses uploaded images automatically and leaves resizing to other tools. Intervention fits when you need to resize or convert images. Spatie fits when you need to shrink them. For more on image file formats for faster, better web performance, consider format-specific compression and conversion strategies.

Testing and CI/CD

Testing and delivery practices decide how safely changes reach production. Laravel teams should pair unit tests for isolated logic with feature tests for request flows, then block pull-request merges behind CI checks.

Unit and feature testing

JetBrains found that 32% skip tests. Laravel makes testing straightforward enough that it should be part of the workflow. Pest default testing has been the default testing framework since Laravel 11, with PHPUnit still fully supported, and Pest 4 builds on PHPUnit 12.

The distinction to keep straight: unit tests in tests/Unit never boot the Laravel application, so they suit isolated logic in models and services. Feature tests in tests/Feature boot the framework and exercise full HTTP request/response cycles. The Laravel testing docs are direct about the balance: "Generally, most of your tests should be feature tests. These types of tests provide the most confidence that your system as a whole is functioning as intended."

A Pest feature test for a JSON API endpoint:

test('making an api request', function () {
    $response = $this->postJson('/api/user', ['name' => 'Sally']);

    $response
        ->assertStatus(201)
        ->assertJson(['created' => true]);
});

The RefreshDatabase trait resets state between tests, and php artisan test --parallel can speed up large suites. For more on API call basics and how to implement them effectively, see our guide.

CI/CD automation

A pull-request workflow helps catch regressions before code merges. This minimal GitHub Actions workflow comes from the official laravel/laravel repository:

name: Tests

on: [pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v5

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          coverage: none

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-interaction --no-progress

      - name: Execute tests
        run: php artisan test

On pull_request triggers, checkout merged result of the PR into the base branch, which catches integration breakage that passing branch tests would miss. This combination of automated testing plus deployment gates helps prevent the "works on my machine" class of production bugs that plagues Laravel projects without CI/CD.

Code maintainability and database hygiene

Use migrations and backups to leave a trail future developers can trust. Migrations and backups make database changes reviewable, reversible, and recoverable when production behaves differently than staging.

Migrations for every schema change

The Laravel docs describe migrations as database version control. In production, schema changes are safer when they go through migrations instead of manual database edits. Each schema change should be a migration file, committed to Git, and run through your CI/CD pipeline. Adding columns to an existing table looks like this:

Schema::table('users', function (Blueprint $table) {
    $table->after('password', function (Blueprint $table) {
        $table->string('address_line1');
        $table->string('address_line2');
        $table->string('city');
    });
});

Write the down method to reverse every up, so rollbacks stay a one-command operation rather than an incident. Understanding code-first vs database-first approaches helps you choose the right data access strategy for your project.

Backup automation

The Laravel backup package automates application and database backups, and it's actively maintained. One compatibility note: v10 requires PHP 8.4 and Laravel 12 or higher, so teams on PHP 8.3 should pin an earlier compatible release. You can schedule backups in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('backup:clean')->daily()->at('01:00');
Schedule::command('backup:run')->daily()->at('01:30');

For offsite snapshots, list an s3 disk (or a Google Cloud Storage Flysystem driver) under destination.disks in config/backup.php. The package's cleanup strategy keeps the newest backup regardless of retention settings, which is exactly the failure mode you want it to guard against.

Using Laravel with a headless CMS

Content-heavy applications often age better when content management and application logic are not forced into the same codebase. A headless CMS can serve content APIs while Laravel keeps control of authentication, business logic, and data processing.

Why pair Laravel with a headless CMS

Laravel is strong at application logic, including routing and API development. Editorial workflows and content modeling usually turn into maintenance work when teams build them from scratch inside a Laravel app, as does the publishing interface. A headless CMS like Strapi handles that layer: your content team gets the Strapi Admin Panel for creating and publishing content, while your engineers keep full control of the application.

In a common setup, Strapi serves the content API while Laravel handles authentication and business logic. The frontend then renders the combined result with Blade, Inertia, or a JavaScript framework. This is the same API-first architecture that lets one content source feed web and mobile channels, along with other surfaces, and it's a natural step for teams looking to move content out of a monolith.

Laravel + Strapi integration

Strapi 5 auto-generates REST endpoints for each Content-Type, and Laravel's HTTP client consumes them cleanly. Combining the fetch with Laravel's caching layer means repeated page loads don't have to hit the CMS:

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;

$articles = Cache::remember('strapi.articles.page1', 300, function () {
    return Http::withToken(config('services.strapi.token'))
        ->get(config('services.strapi.url') . '/api/articles', [
            'populate' => '*',
        ])
        ->json('data');
});

For v4 migrations, keep three response details in mind:

  • First, the response format is flattened: fields sit directly on the data object, so read $article['title'], not $article['attributes']['title'].
  • Second, the string documentId is now the canonical identifier for subsequent API calls. It replaces the numeric id.
  • By default, responses include only top-level scalar fields; use the populate parameter beyond top-level scalar fields for relations and media. Components use the same mechanism, covered in depth in Strapi populate guide.

These details are usually where older integrations break.

Strapi supports both REST and GraphQL, and on the Strapi side you can add response caching with the REST Cache plugin. The Laravel integration tutorial covers a complete Strapi and Laravel build. If managed hosting would take CMS infrastructure off your plate, Strapi Cloud can handle hosting and backups, with CDN delivery included.

Building production-ready Laravel applications

Laravel's strength comes from its conventions. When you follow them, the framework carries most of the weight: PSR compliance, fat models, Form Request validation, config caching, eager loading, automated testing, and migration hygiene form the baseline for production-quality Laravel code. If your application serves content-heavy pages, consider pairing it with Strapi 5 as a headless CMS backend. You get a structured content API your Laravel app can consume with a few lines of HTTP client code, plus an Admin Panel your content team can use without filing engineering tickets. If managed CMS hosting fits your workflow, Strapi Cloud is an option.

Paul BratslavskyDeveloper Advocate

Related Posts

How tos·7 min read

Using Strapi and Laravel

In this article, we will look at how Strapi & Laravel can be used together.

·September 16, 2021
Definitions & benefits·6 min read

10 Reasons Why Developers Should Use an API CMS

In this article, we'll be giving 10 reasons why you should use an API CMS as a developer including Cross-Platform Technology, User-Friendly, Cross-platform C...

·November 2, 2021
Integrate Strapi in Developer Teams for Tech Websites
Use Cases·13 min read

4 Steps to Integrate Strapi in Developer Teams for Tech Websites

Transform your developer workflow with Strapi integration. Enjoy flexibility, customization, and seamless API management in all your tech projects.

·September 6, 2024