Laravel Patterns I Use on Every Project (And Why)
Some of these will be obvious. Some will be things you've heard before but never actually done. A few might be controversial. I don't care — these are the patterns I apply on every new Laravel project because I've seen what happens when they're absent.
Thin Controllers
A controller method should do roughly three things: validate input, call something to do the work, return a response. If I'm writing business logic in a controller, something's wrong.
// I write controllers that look like this
class PostController extends Controller
{
public function store(StorePostRequest $request, CreatePost $action): RedirectResponse
{
$post = $action->execute($request->validated(), $request->user());
return redirect()->route('posts.show', $post)
->with('success', 'Post published.');
}
}
The CreatePost action class handles the actual work. It can be tested independently of HTTP concerns. When the requirements change, I know exactly where to look.
Form Requests — Always
Validation and authorization in one place, out of the controller:
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->can('create', Post::class);
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'content' => ['required', 'string', 'min:100'],
'tags' => ['array', 'max:5'],
'tags.*' => ['string', 'max:50'],
];
}
}
The authorization in authorize() runs before validation. Unauthorized requests never make it to the rules — they get a 403 immediately.
API Resources: Every Single Time
Returning raw Eloquent models means your database schema is your public API. When you rename a column, you break integrations. When you add a sensitive field, you might accidentally expose it. API Resources create a deliberate layer between your DB and your responses.
Prevent Lazy Loading in Development
I already wrote a whole post about N+1 queries, but this deserves repeating: add this to your AppServiceProvider right now:
Model::preventLazyLoading(! app()->isProduction());
It costs nothing and catches an entire category of performance bugs before they reach production.
Typed Everything
PHP 8.4 has excellent type system support. I use it:
class NotificationService
{
public function sendWelcome(User $user): void
{
$user->notify(new WelcomeNotification);
}
/** @return Collection<int, Notification> */
public function recent(User $user, int $limit = 10): Collection
{
return $user->notifications()->limit($limit)->get();
}
}
Static analysis tools (PHPStan with Larastan) can catch real bugs before you run the code. I run it in CI on every pull request.
Test Every Route
When people ask me "how do I write better code?", the answer is almost always: write tests first. Not TDD as a religion, just the discipline of having a test for every endpoint.
it('allows authors to delete their own posts', function () {
$author = User::factory()->create();
$post = Post::factory()->for($author)->create();
$this->actingAs($author)
->deleteJson(route('posts.destroy', $post))
->assertNoContent();
assertModelMissing($post);
});
it('prevents non-authors from deleting posts', function () {
$post = Post::factory()->create();
$this->actingAs(User::factory()->create())
->deleteJson(route('posts.destroy', $post))
->assertForbidden();
});
Use Enums for Status Fields
String status fields like is_active and status = 'pending' are fine until someone typos "pendng" somewhere in the codebase. Backed enums eliminate that:
enum PostStatus: string
{
case Draft = 'draft';
case Published = 'published';
case Archived = 'archived';
public function label(): string
{
return match($this) {
self::Draft => 'Draft',
self::Published => 'Published',
self::Archived => 'Archived',
};
}
}
// In the model
protected function casts(): array
{
return ['status' => PostStatus::class];
}
// In a query
Post::where('status', PostStatus::Published)->get();
Queue Everything That Can Wait
If a task doesn't need to complete before the user sees a response, queue it. Sending a welcome email, generating a PDF, indexing a document in Elasticsearch, notifying Slack — all of these can be queued. The response time your users experience is directly affected by this discipline.