Back to the latest insights
Laravel3 min read

8 Laravel Best Practices Every Developer Should Know

Laravel makes building apps fast — but the framework can't save you from bad habits. A clean Laravel codebase isn't an accident; it comes from a handful of disciplined practices applied consistently from day one.

laravel tips and tricks
laravel tricks

8 Laravel Best Practices Every Developer Should Build Early

mohibsoftware.com  |  Laravel Development

Laravel makes building apps fast — but the framework can't save you from bad habits. A clean Laravel codebase isn't an accident; it comes from a handful of disciplined practices applied consistently from day one.

Here's what I wish every Laravel developer practiced early.

1. Keep Controllers Thin

Controllers should route, authorize, and return responses. Business logic belongs in Actions or Services — not 200-line controller methods.

// Bad: fat controller
public function store(Request $request)
{
    $validated = $request->validate([...]);
    $order = Order::create($validated);
    $order->items()->createMany($request->items);
    Mail::to($order->user)->send(new OrderConfirmed($order));
    // ...20 more lines
}
 
// Good: thin controller
public function store(StoreOrderRequest $request, CreateOrder $createOrder)
{
    $order = $createOrder->handle($request->validated());
 
    return new OrderResource($order);
}

2. Validate with Form Requests

Don't sprinkle $request->validate() everywhere. Dedicated Form Request classes keep rules reusable, readable, and testable.

php artisan make:request StoreOrderRequest
 
class StoreOrderRequest extends FormRequest
{
    public function authorize(): bool
    {
        return $this->user()->can('create', Order::class);
    }
 
    public function rules(): array
    {
        return [
            'items' => ['required', 'array', 'min:1'],
            'items.*.product_id' => ['required', 'exists:products,id'],
        ];
    }
}

3. Prefer Eloquent Wisely — Know When to Query

Eloquent is great until N+1 queries show up. Use with(), load(), and check Telescope or Debugbar before blaming "slow PHP."

// Bad: N+1 query
$orders = Order::all();
foreach ($orders as $order) {
    echo $order->user->name; // triggers a query per order
}
 
// Good: eager loading
$orders = Order::with('user')->get();

4. Queues Are Not Optional for Slow Work

Emails, image processing, webhooks, reports — put them on queues. Your users shouldn't wait for background work to finish in the HTTP request.

class ProcessOrderExport implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
    public function __construct(public Order $order) {}
 
    public function handle(): void
    {
        // heavy work happens off the request cycle
    }
}
 
ProcessOrderExport::dispatch($order);

5. Use Jobs, Events, and Listeners for Side Effects

If something happens "because X occurred," model it as an event. It keeps your core flow clean and makes future features easier to plug in.

// Fire the event
event(new OrderShipped($order));
 
// Listener reacts independently
class SendShipmentNotification
{
    public function handle(OrderShipped $event): void
    {
        Notification::send($event->order->user, new ShipmentSent($event->order));
    }
}

6. Cache the Expensive Stuff (Carefully)

Config, routes, and views in production — yes. Also cache heavy queries with clear invalidation keys. A bad cache strategy is worse than no cache.

php artisan config:cache
php artisan route:cache
php artisan view:cache
 
// Cache with explicit invalidation
$products = Cache::remember('products.featured', now()->addHours(6), function () {
    return Product::where('featured', true)->get();
});
 
// Invalidate when data changes
Cache::forget('products.featured');

7. Write Feature Tests for Critical Paths

Login, checkout, permissions, payments, webhooks. You don't need 100% coverage — you need confidence on the flows that can break the business.

public function test_authenticated_user_can_checkout(): void
{
    $user = User::factory()->create();
    $product = Product::factory()->create(['stock' => 5]);
 
    $response = $this->actingAs($user)
        ->post('/checkout', ['product_id' => $product->id, 'qty' => 1]);
 
    $response->assertStatus(201);
    $this->assertDatabaseHas('orders', ['user_id' => $user->id]);
}

8. Use Policies and Gates for Authorization

Authorization scattered across controllers becomes a security mess. Centralize it. Then reuse it in controllers, Blade, and API resources.

class OrderPolicy
{
    public function view(User $user, Order $order): bool
    {
        return $user->id === $order->user_id || $user->isAdmin();
    }
}
 
// Controller
$this->authorize('view', $order);
 
// Blade
@can('view', $order)
    <a href="{{ route('orders.show', $order) }}">View Order</a>
@endcan

Final Thought

Ship less "clever" code. Ship clear, testable Laravel code your future self can maintain.

Which Laravel tip changed how you build apps the most? Let me know in the comments.

Have a project in mind?

Let’s turn the useful idea into a useful website.

Start with two simple questions