Laravel Best Practices I Wish I Knew 3 Years Ago
By Hardik Kanajariya — full‑stack dev who learned a lot the hard way so you don’t have to.
A quick hello 👋
When I started with Laravel, I built shippable stuff fast… and created future headaches even faster. After 3+ years in the trenches (client apps, side projects, and refactors I’m still emotionally processing), here are the practices I wish I knew on day one. If you’re a beginner or intermediate dev, this is the blueprint I’d hand to my younger self.
1) Design your project by domains, not folders
Flat Controllers/Models/Requests
directories scale like spaghetti. Group features by bounded context: Auth, Billing, Catalog, Checkout, etc. This keeps code co-located and reviewable.
Structure
app/
Domain/
Catalog/
Actions/
DTOs/
Events/
Http/
Controllers/
Requests/
Resources/
Models/
Policies/
Queries/
Services/
routes.php
routes/web.php
// routes/web.php
app/Domain/Catalog/routes.php
use
Why it slaps: Onboarding is faster, coupling is lower, and features read like chapters not scattered sentences.
Common pitfalls
Dumping everything into
Services/
as a junk drawer.Sharing Models across domains without anti-corruption layers.
2) Embrace the Service Container & Dependency Injection
Stop new-ing classes in controllers. Ask the container for dependencies and make code testable.
Controller with DI
namespace
Binding interfaces
// app/Providers/AppServiceProvider.php
Pro tip: prefer bind()
for per-resolve, singleton()
for a shared instance, and scoped()
for per-request.
Smells to avoid
new StripeGateway(...)
inside controllers (hard to swap/test).Static facades everywhere — use them sparingly in app code, lean on DI.
3) Form Requests are your first firewall
Validate and authorize at the edge. Keep controllers skinny.
Form Request
// app/Domain/Catalog/Http/Requests/StoreProductRequest.php
Controller
public
Bonus: Return JsonResponse
on validation failure automatically for API routes.
Mistakes
Validating in controllers or models. Duplication guaranteed.
Not using
bail
to short-circuit noisy errors.
4) Model your database like a grown-up
Design tables with purpose. Normalize first, denormalize later for read performance.
Migrations
Schema::create(
Relationships
class
Indexes matter
$table->index([
Avoid
Storing arrays/JSON for relational data you filter on (hard to index).
Nullable FKs everywhere — model real-world rules with constraints.
5) Smart caching: cache truth, not lies
Use cache as a read accelerator, not a source of truth.
Query caching with tags
use
Invalidate on write
public
Response caching (API)
// Example with spatie/laravel-responsecache (see packages)
Gotchas
Never cache per-user data without keys that include the user ID.
Don’t forget to set a driver like Redis in production.
6) Security must‑dos: treat prod like a crime scene
Checklist vibes but mandatory.
Basics baked-in
CSRF on POST/PUT/PATCH/DELETE (Blade
@csrf
orcsrf_token()
).Mass assignment: use
$fillable
or guarded DTOs.Always authorize: policies or gates — not if/else in controllers.
Examples
// Policy
Headers/Middleware
// app/Http/Kernel.php
.env hygiene
Never commit
.env
or.env.*
.Rotate keys (
php artisan key:generate
) if leaked.
Avoid
Building your own auth. Use Laravel Breeze/Fortify/Passport/Sanctum.
Storing secrets in config files — use env or Vault.
7) Performance tuning: make it fast by default
Start with visibility, then fix the hotspots.
Eager load
// Bad: N+1
Chunk & queue
Order::where(
Indexes & EXPLAIN
EXPLAIN
Config & route cache
php artisan config:cache
php artisan route:cache
php artisan view:cache
Octane (when it fits)
If your app is I/O heavy and stateless, Laravel Octane with Swoole/RoadRunner is a W.
Avoid
Micro-optimizing PHP before fixing queries.
Caching broken queries — you’ll just serve bad data faster.
8) Build a testing culture, not just tests
Aim for confidence: key flows, domain services, policies, and edge cases.
Pest example (my go-to)
// tests/Feature/CreateProductTest.php
Fakes and spies
Storage::fake(
Run it
php artisan
Avoid
Only testing controllers. Your core logic should live in services/actions and be unit-testable.
Brittle snapshot tests for HTML — prefer feature tests that assert behavior.
9) Environment config hygiene
Keep secrets and settings sane across dev/stage/prod.
.env.example
APP_NAME=
Per‑env config
// config/services.php
Secrets
Use environment variables in CI/CD. For Docker/Kubernetes, mount secrets, don’t bake them into images.
Prefer parameter stores (AWS SSM/Secrets Manager, Vault) for production.
Avoid
Mixing config logic into
.env
(keep.env
dumb; logic lives inconfig/*
).
10) Bonus: API resources & DTOs for clean boundaries
Don’t return Eloquent models raw. Shape your output.
API Resource
// app/Domain/Catalog/Http/Resources/ProductResource.php
DTO (Spatie data)
// Using spatie/laravel-data
Recommended packages that actually earn their keep
spatie/laravel-permission — roles & permissions done right.
spatie/laravel-data — typed DTOs with validation & casting.
spatie/laravel-responsecache — cache entire responses.
laravel/pint — opinionated code style, zero bikeshedding.
barryvdh/laravel-debugbar — quick visibility in dev (never prod).
spatie/laravel-query-builder — filter/sort includes for APIs.
pestphp/pest — minimal, readable tests with great DX.
laravel/octane — long‑running workers for perf.
laravel/telescope — request/DB/job insights in non‑prod.
Common mistakes I see (and made 🙃)
Treating Eloquent models as god objects: fat models, anemic services. Split responsibilities.
Skipping database indexes. If it’s in a WHERE or JOIN, it probably deserves an index.
Pushing everything through observers/events without a domain story. Over‑architecting is a tax.
Forgetting queue failures & retries — configure
failed_jobs
table and retry logic.Not measuring. Add metrics/logging before guessing.
Copy‑paste checklist ✅
Features grouped by Domain/ with routes per domain
Dependencies injected; interfaces bound in providers
Form Requests for validation + authorization
Migrations with constraints, FKs, and indexes
Caching with tags; flush on writes
Policies/Gates enforced; CSRF, mass‑assignment, headers
N+1 killed; heavy jobs queued; configs cached
Tests (Pest) for services, policies, critical flows
.env.example
complete; secrets in SSM/Secrets Manager/VaultAPI Resources/DTOs for clean IO
Final words
Laravel makes it easy to ship. These practices make it easy to maintain. Start small: pick one domain, add a Form Request, bind an interface, write one Pest test. Momentum compounds.
If this saved you a future refactor, my work here is done. Now go delete that 500‑line controller. You know the one. 😉