Laravel Best Practices for Production Apps in 2025

The patterns, principles, and practices I use across 100+ production Laravel applications. Covers architecture, performance, security, and team collaboration.

AC
Aqib Chaudhary
·April 20, 2025·10 min read
LaravelPHPBackendBest PracticesArchitecture

Why Laravel Best Practices Matter More Than Ever

Laravel powers over 3 million websites. It's the most popular PHP framework for good reason — it's productive, expressive, and has an incredible ecosystem. But with great power comes great responsibility.

Bad Laravel code is really bad. I've inherited codebases where:

  • Models had 2,000+ lines
  • Controllers were doing everything
  • N+1 queries were causing 30-second page loads
  • No tests existed, so every change was a gamble

This guide is the result of 10+ years of Laravel development and 100+ production applications.

Architecture Principles

1. Fat Models, Skinny Controllers — Done Right

The old advice was "fat models, skinny controllers." This led to God models with 2,000+ lines. The right approach:

Controllers: Only HTTP layer concerns (validation, calling services, returning responses) Services: Business logic Models: Eloquent relationships, scopes, casts — no business logic Actions: Single-responsibility classes for complex operations

// ❌ Bad: Logic in controller
class InvoiceController extends Controller
{
    public function store(Request $request)
    {
        // 50 lines of business logic here
    }
}

// ✅ Good: Delegate to action
class InvoiceController extends Controller
{
    public function store(StoreInvoiceRequest $request, CreateInvoice $action)
    {
        $invoice = $action->execute($request->validated());
        return InvoiceResource::make($invoice);
    }
}

2. Form Requests Are Non-Negotiable

Every POST/PUT/PATCH endpoint needs a Form Request. No exceptions. This gives you:

  • Clean validation out of controllers
  • Reusable validation logic
  • Authorization in one place
  • Auto-documentation potential

3. API Resources Always

Never return Eloquent models directly from API endpoints. Always use API Resources. This:

  • Prevents data leaks (exposing internal fields)
  • Gives you a transformation layer
  • Makes API versioning possible

Performance

Eager Loading is Not Optional

N+1 queries will kill your app at scale. Use Laravel Telescope in development to catch them before they reach production.

// ❌ Causes N+1
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->customer->name; // Separate query per order
}

// ✅ Eager load
$orders = Order::with(['customer', 'items.product'])->get();

Database Indexing Strategy

For every column you filter by, order by, or join on — add an index. Use EXPLAIN to verify query plans.

Queue Everything Non-Critical

Email sending, notifications, PDF generation, third-party API calls — all of it goes in a queue. Your HTTP response time should never depend on external services.

Security Essentials

  1. Always use Form Requests for authorization — check policies in authorize()
  2. Never trust user input in queries — Eloquent protects you, raw queries don't
  3. Rotate application keys on every environment change
  4. Use signed URLs for one-time links (email verification, password reset)
  5. Rate limit everything using throttle middleware

Testing Strategy

My minimum test coverage for production apps:

  • Feature tests for every API endpoint
  • Unit tests for service classes and complex logic
  • Browser tests (Dusk) for critical user flows only

The goal isn't 100% coverage. The goal is confidence that your critical paths work.

test('user can create an invoice', function () {
    $user = User::factory()->create();
    $customer = Customer::factory()->create(['user_id' => $user->id]);

    $response = $this->actingAs($user)
        ->postJson('/api/invoices', [
            'customer_id' => $customer->id,
            'items' => [['description' => 'Service', 'amount' => 1000]],
        ]);

    $response->assertCreated();
    $this->assertDatabaseHas('invoices', ['customer_id' => $customer->id]);
});

The Packages I Install in Every Project

  • Spatie Laravel Permission — roles and permissions
  • Spatie Laravel Activitylog — audit trails
  • Spatie Laravel Media Library — file management
  • Laravel Telescope — debugging (dev only)
  • Laravel Horizon — queue monitoring
  • Sentry — error tracking
  • Filament — admin panels

Closing Thoughts

Good Laravel code is boring. It's consistent, predictable, and easy to change. The most impressive code I've written is the code that future developers never have to think about.

Write boring code. Ship fast. Sleep well.