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.phpapp/Domain/Catalog/routes.php
useWhy 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
namespaceBinding interfaces
// app/Providers/AppServiceProvider.phpPro 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.phpController
publicBonus: Return JsonResponse on validation failure automatically for API routes.
Mistakes
Validating in controllers or models. Duplication guaranteed.
Not using
bailto 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
classIndexes 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
useInvalidate on write
publicResponse 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
@csrforcsrf_token()).Mass assignment: use
$fillableor guarded DTOs.Always authorize: policies or gates — not if/else in controllers.
Examples
// PolicyHeaders/Middleware
// app/Http/Kernel.php.env hygiene
Never commit
.envor.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+1Chunk & queue
Order::where(Indexes & EXPLAIN
EXPLAINConfig & 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.phpFakes 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.phpSecrets
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.envdumb; 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.phpDTO (Spatie data)
// Using spatie/laravel-dataRecommended 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_jobstable 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.examplecomplete; 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. 😉