Best Practices

This final lesson doesn't introduce anything new — it pulls together the habits, from every earlier lesson in this track, that separate code that merely "works" from a Laravel application that stays pleasant to maintain a year later. Treat it as a checklist to revisit on real projects, not just a one-time read.

1. Keep Controllers Thin

A controller's job, covered in Controllers, is to receive the request and return a response — not to host business logic. When a controller method grows past a handful of lines of actual decision-making, that logic belongs somewhere more reusable: the model, a dedicated service class, or a Form Request.

<?php
// Thin: the controller delegates, it doesn't decide
public function store(StoreOrderRequest $request)
{
    $order = $this->orders->createFromRequest($request->validated());

    return redirect()->route('orders.show', $order);
}
  • Push validation into a Form Request, not inline if checks in the controller.
  • Push multi-step business logic into a model method or a small service class.
  • A controller method that reads like a short summary of "what happens," not "how," is a good sign.

2. Use Form Requests for Real Validation

As covered in Requests & Validation, any validation beyond a couple of trivial rules belongs in its own Form Request class, not inline in the controller.

php artisan make:request StoreOrderRequest
  • Keeps validation rules testable and reusable on their own.
  • Authorization (authorize()) travels with the rules, not scattered separately.
  • The controller signature alone (type-hinting the Form Request) documents what's expected.

3. Always Eager-Load Relationships

The N+1 query problem, covered in Eloquent Relationships, is one of the easiest performance mistakes to make and one of the easiest to avoid — the fix is a single with() call.

<?php
$authors = Author::with('articles')->get(); // 2 queries, not N+1
  • Any time you loop over a collection and touch a relationship, ask whether it's eager-loaded.
  • In development, tools like Laravel Debugbar make N+1 problems visible immediately — check query counts, not just correctness.

4. Use API Resources, Never Raw Models

As covered in API Resources & REST APIs, returning a model directly from an API endpoint leaks whatever columns happen to exist on the table — including ones that were never meant to be public.

<?php
return ArticleResource::collection(Article::with('author')->get());
  • A Resource is the one place that decides the JSON shape — controllers and models don't need to know or care.
  • Adding a new column to the table never silently changes your public API response.

5. Push Slow Work Onto Queued Jobs

As covered in Jobs & Queues, anything that doesn't need to finish before the response is sent — sending an email, generating a report, calling a slow external API — belongs on a queue, not inline in the request.

<?php
ProcessPodcastUpload::dispatch($podcast); // returns instantly, work happens in the background
  • A request that blocks for 5+ seconds on non-essential work is a queue candidate.
  • Keeps the user-facing response fast regardless of how slow the background work is.

6. Use Policies, Not Scattered if Checks

As covered in Authorization: Gates & Policies, model-specific "can this user do this to this record?" logic belongs in a Policy class, checked consistently via $this->authorize() or @can, not repeated inline through every controller.

<?php
public function update(Request $request, Article $article)
{
    $this->authorize('update', $article);
    // ...
}
  • One place per model to find and change the authorization rule.
  • Consistent everywhere — a controller, a Blade @can directive, and an API endpoint all defer to the same Policy.

7. Write Feature Tests for Critical Flows

As covered in Testing, you don't need 100% coverage to get real value — but the flows that would genuinely hurt if they silently broke (checkout, registration, login, anything involving money) deserve at least one Feature test each.

<?php
$response = $this->post('/checkout', $validPayload);
$response->assertStatus(200);
  • Start with the flows that would be a genuine incident if they broke, not exhaustive coverage everywhere.
  • A Feature test that hits the real route is a stronger safety net than a Unit test of an isolated helper.

8. Never Disable CSRF or Use $guarded = [] "To Make It Work"

As covered in Security Best Practices, both of these show up constantly as a quick fix for a confusing error — and both quietly remove a real protection.

  • A 419 error means a form is missing @csrf — add it, don't exempt the route from CSRF verification.
  • A mass-assignment exception means a column is missing from $fillable — add that column by name, don't switch to $guarded = [].

9. Bind Interfaces, Not Concrete Classes

As covered in Service Container & DI, when a piece of logic might reasonably be swapped later (a different payment gateway, a fake implementation in tests), depend on an interface and let the service container resolve the concrete class.

<?php
// AppServiceProvider::register()
$this->app->bind(PaymentGateway::class, StripeGateway::class);
  • Type-hint the interface in constructors, never the concrete class, when this flexibility matters.
  • Makes swapping a real dependency for a test double trivial, without touching the class that uses it.

10. Lean on Laravel's Conventions

Every lesson in this track has followed a consistent thread: Laravel has an opinion about where things go and how they're named — model names implying table names, {model}_id foreign keys, resourceful controller method names, the app/ directory structure — and fighting those conventions for no strong reason usually costs more than it saves.

  • Reach for Laravel's built-in solution first (a Form Request, a Policy, a queued Job) before writing a custom one.
  • When you do deviate from convention, know why — and expect the next person reading the code to ask.
You've completed the Laravel track. That's 30 lessons, from your very first route to broadcasting real-time updates over WebSockets, and it genuinely covers what a production Laravel application looks like end to end — not just isolated tricks, but how routing, Eloquent, middleware, authentication, jobs, events, testing, and security actually fit together as one working system. That mental model is the real payoff here, and it's the same one that carries over directly into any real Laravel codebase you touch next. This is also the most comprehensive track on the entire site, so the "Next" button below leads back to the PHP hub — completing every track this site offers. Congratulations, and thank you for following it all the way through.