[{"data":1,"prerenderedAt":531},["ShallowReactive",2],{"blog-laravel-best-practices-2025":3},{"id":4,"title":5,"body":6,"category":516,"date":517,"description":518,"extension":519,"featured":520,"meta":521,"navigation":140,"path":522,"readTime":144,"seo":523,"stem":524,"tags":525,"__hash__":530},"blog\u002Fblog\u002Flaravel-best-practices-2025.md","Laravel Best Practices for Production Apps in 2025",{"type":7,"value":8,"toc":499},"minimark",[9,14,18,26,42,45,49,54,57,75,191,195,198,212,216,219,230,234,238,241,284,288,295,299,302,306,339,343,346,366,369,437,441,485,489,492,495],[10,11,13],"h2",{"id":12},"why-laravel-best-practices-matter-more-than-ever","Why Laravel Best Practices Matter More Than Ever",[15,16,17],"p",{},"Laravel powers over 3 million websites. It's the most popular PHP framework for good reason — it's productive, expressive, and has an incredible ecosystem. But with great power comes great responsibility.",[15,19,20,21,25],{},"Bad Laravel code is ",[22,23,24],"strong",{},"really"," bad. I've inherited codebases where:",[27,28,29,33,36,39],"ul",{},[30,31,32],"li",{},"Models had 2,000+ lines",[30,34,35],{},"Controllers were doing everything",[30,37,38],{},"N+1 queries were causing 30-second page loads",[30,40,41],{},"No tests existed, so every change was a gamble",[15,43,44],{},"This guide is the result of 10+ years of Laravel development and 100+ production applications.",[10,46,48],{"id":47},"architecture-principles","Architecture Principles",[50,51,53],"h3",{"id":52},"_1-fat-models-skinny-controllers-done-right","1. Fat Models, Skinny Controllers — Done Right",[15,55,56],{},"The old advice was \"fat models, skinny controllers.\" This led to God models with 2,000+ lines. The right approach:",[15,58,59,62,63,66,67,70,71,74],{},[22,60,61],{},"Controllers",": Only HTTP layer concerns (validation, calling services, returning responses)\n",[22,64,65],{},"Services",": Business logic\n",[22,68,69],{},"Models",": Eloquent relationships, scopes, casts — no business logic\n",[22,72,73],{},"Actions",": Single-responsibility classes for complex operations",[76,77,82],"pre",{"className":78,"code":79,"language":80,"meta":81,"style":81},"language-php shiki shiki-themes github-light github-dark","\u002F\u002F ❌ Bad: Logic in controller\nclass InvoiceController extends Controller\n{\n    public function store(Request $request)\n    {\n        \u002F\u002F 50 lines of business logic here\n    }\n}\n\n\u002F\u002F ✅ Good: Delegate to action\nclass InvoiceController extends Controller\n{\n    public function store(StoreInvoiceRequest $request, CreateInvoice $action)\n    {\n        $invoice = $action->execute($request->validated());\n        return InvoiceResource::make($invoice);\n    }\n}\n","php","",[83,84,85,93,99,105,111,117,123,129,135,142,148,153,158,164,169,175,181,186],"code",{"__ignoreMap":81},[86,87,90],"span",{"class":88,"line":89},"line",1,[86,91,92],{},"\u002F\u002F ❌ Bad: Logic in controller\n",[86,94,96],{"class":88,"line":95},2,[86,97,98],{},"class InvoiceController extends Controller\n",[86,100,102],{"class":88,"line":101},3,[86,103,104],{},"{\n",[86,106,108],{"class":88,"line":107},4,[86,109,110],{},"    public function store(Request $request)\n",[86,112,114],{"class":88,"line":113},5,[86,115,116],{},"    {\n",[86,118,120],{"class":88,"line":119},6,[86,121,122],{},"        \u002F\u002F 50 lines of business logic here\n",[86,124,126],{"class":88,"line":125},7,[86,127,128],{},"    }\n",[86,130,132],{"class":88,"line":131},8,[86,133,134],{},"}\n",[86,136,138],{"class":88,"line":137},9,[86,139,141],{"emptyLinePlaceholder":140},true,"\n",[86,143,145],{"class":88,"line":144},10,[86,146,147],{},"\u002F\u002F ✅ Good: Delegate to action\n",[86,149,151],{"class":88,"line":150},11,[86,152,98],{},[86,154,156],{"class":88,"line":155},12,[86,157,104],{},[86,159,161],{"class":88,"line":160},13,[86,162,163],{},"    public function store(StoreInvoiceRequest $request, CreateInvoice $action)\n",[86,165,167],{"class":88,"line":166},14,[86,168,116],{},[86,170,172],{"class":88,"line":171},15,[86,173,174],{},"        $invoice = $action->execute($request->validated());\n",[86,176,178],{"class":88,"line":177},16,[86,179,180],{},"        return InvoiceResource::make($invoice);\n",[86,182,184],{"class":88,"line":183},17,[86,185,128],{},[86,187,189],{"class":88,"line":188},18,[86,190,134],{},[50,192,194],{"id":193},"_2-form-requests-are-non-negotiable","2. Form Requests Are Non-Negotiable",[15,196,197],{},"Every POST\u002FPUT\u002FPATCH endpoint needs a Form Request. No exceptions. This gives you:",[27,199,200,203,206,209],{},[30,201,202],{},"Clean validation out of controllers",[30,204,205],{},"Reusable validation logic",[30,207,208],{},"Authorization in one place",[30,210,211],{},"Auto-documentation potential",[50,213,215],{"id":214},"_3-api-resources-always","3. API Resources Always",[15,217,218],{},"Never return Eloquent models directly from API endpoints. Always use API Resources. This:",[27,220,221,224,227],{},[30,222,223],{},"Prevents data leaks (exposing internal fields)",[30,225,226],{},"Gives you a transformation layer",[30,228,229],{},"Makes API versioning possible",[10,231,233],{"id":232},"performance","Performance",[50,235,237],{"id":236},"eager-loading-is-not-optional","Eager Loading is Not Optional",[15,239,240],{},"N+1 queries will kill your app at scale. Use Laravel Telescope in development to catch them before they reach production.",[76,242,244],{"className":78,"code":243,"language":80,"meta":81,"style":81},"\u002F\u002F ❌ Causes N+1\n$orders = Order::all();\nforeach ($orders as $order) {\n    echo $order->customer->name; \u002F\u002F Separate query per order\n}\n\n\u002F\u002F ✅ Eager load\n$orders = Order::with(['customer', 'items.product'])->get();\n",[83,245,246,251,256,261,266,270,274,279],{"__ignoreMap":81},[86,247,248],{"class":88,"line":89},[86,249,250],{},"\u002F\u002F ❌ Causes N+1\n",[86,252,253],{"class":88,"line":95},[86,254,255],{},"$orders = Order::all();\n",[86,257,258],{"class":88,"line":101},[86,259,260],{},"foreach ($orders as $order) {\n",[86,262,263],{"class":88,"line":107},[86,264,265],{},"    echo $order->customer->name; \u002F\u002F Separate query per order\n",[86,267,268],{"class":88,"line":113},[86,269,134],{},[86,271,272],{"class":88,"line":119},[86,273,141],{"emptyLinePlaceholder":140},[86,275,276],{"class":88,"line":125},[86,277,278],{},"\u002F\u002F ✅ Eager load\n",[86,280,281],{"class":88,"line":131},[86,282,283],{},"$orders = Order::with(['customer', 'items.product'])->get();\n",[50,285,287],{"id":286},"database-indexing-strategy","Database Indexing Strategy",[15,289,290,291,294],{},"For every column you filter by, order by, or join on — add an index. Use ",[83,292,293],{},"EXPLAIN"," to verify query plans.",[50,296,298],{"id":297},"queue-everything-non-critical","Queue Everything Non-Critical",[15,300,301],{},"Email sending, notifications, PDF generation, third-party API calls — all of it goes in a queue. Your HTTP response time should never depend on external services.",[10,303,305],{"id":304},"security-essentials","Security Essentials",[307,308,309,315,321,327,333],"ol",{},[30,310,311,314],{},[22,312,313],{},"Always use Form Requests for authorization"," — check policies in authorize()",[30,316,317,320],{},[22,318,319],{},"Never trust user input in queries"," — Eloquent protects you, raw queries don't",[30,322,323,326],{},[22,324,325],{},"Rotate application keys"," on every environment change",[30,328,329,332],{},[22,330,331],{},"Use signed URLs"," for one-time links (email verification, password reset)",[30,334,335,338],{},[22,336,337],{},"Rate limit everything"," using throttle middleware",[10,340,342],{"id":341},"testing-strategy","Testing Strategy",[15,344,345],{},"My minimum test coverage for production apps:",[27,347,348,354,360],{},[30,349,350,353],{},[22,351,352],{},"Feature tests"," for every API endpoint",[30,355,356,359],{},[22,357,358],{},"Unit tests"," for service classes and complex logic",[30,361,362,365],{},[22,363,364],{},"Browser tests"," (Dusk) for critical user flows only",[15,367,368],{},"The goal isn't 100% coverage. The goal is confidence that your critical paths work.",[76,370,372],{"className":78,"code":371,"language":80,"meta":81,"style":81},"test('user can create an invoice', function () {\n    $user = User::factory()->create();\n    $customer = Customer::factory()->create(['user_id' => $user->id]);\n\n    $response = $this->actingAs($user)\n        ->postJson('\u002Fapi\u002Finvoices', [\n            'customer_id' => $customer->id,\n            'items' => [['description' => 'Service', 'amount' => 1000]],\n        ]);\n\n    $response->assertCreated();\n    $this->assertDatabaseHas('invoices', ['customer_id' => $customer->id]);\n});\n",[83,373,374,379,384,389,393,398,403,408,413,418,422,427,432],{"__ignoreMap":81},[86,375,376],{"class":88,"line":89},[86,377,378],{},"test('user can create an invoice', function () {\n",[86,380,381],{"class":88,"line":95},[86,382,383],{},"    $user = User::factory()->create();\n",[86,385,386],{"class":88,"line":101},[86,387,388],{},"    $customer = Customer::factory()->create(['user_id' => $user->id]);\n",[86,390,391],{"class":88,"line":107},[86,392,141],{"emptyLinePlaceholder":140},[86,394,395],{"class":88,"line":113},[86,396,397],{},"    $response = $this->actingAs($user)\n",[86,399,400],{"class":88,"line":119},[86,401,402],{},"        ->postJson('\u002Fapi\u002Finvoices', [\n",[86,404,405],{"class":88,"line":125},[86,406,407],{},"            'customer_id' => $customer->id,\n",[86,409,410],{"class":88,"line":131},[86,411,412],{},"            'items' => [['description' => 'Service', 'amount' => 1000]],\n",[86,414,415],{"class":88,"line":137},[86,416,417],{},"        ]);\n",[86,419,420],{"class":88,"line":144},[86,421,141],{"emptyLinePlaceholder":140},[86,423,424],{"class":88,"line":150},[86,425,426],{},"    $response->assertCreated();\n",[86,428,429],{"class":88,"line":155},[86,430,431],{},"    $this->assertDatabaseHas('invoices', ['customer_id' => $customer->id]);\n",[86,433,434],{"class":88,"line":160},[86,435,436],{},"});\n",[10,438,440],{"id":439},"the-packages-i-install-in-every-project","The Packages I Install in Every Project",[27,442,443,449,455,461,467,473,479],{},[30,444,445,448],{},[22,446,447],{},"Spatie Laravel Permission"," — roles and permissions",[30,450,451,454],{},[22,452,453],{},"Spatie Laravel Activitylog"," — audit trails",[30,456,457,460],{},[22,458,459],{},"Spatie Laravel Media Library"," — file management",[30,462,463,466],{},[22,464,465],{},"Laravel Telescope"," — debugging (dev only)",[30,468,469,472],{},[22,470,471],{},"Laravel Horizon"," — queue monitoring",[30,474,475,478],{},[22,476,477],{},"Sentry"," — error tracking",[30,480,481,484],{},[22,482,483],{},"Filament"," — admin panels",[10,486,488],{"id":487},"closing-thoughts","Closing Thoughts",[15,490,491],{},"Good Laravel code is boring. It's consistent, predictable, and easy to change. The most impressive code I've written is the code that future developers never have to think about.",[15,493,494],{},"Write boring code. Ship fast. Sleep well.",[496,497,498],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":81,"searchDepth":95,"depth":95,"links":500},[501,502,507,512,513,514,515],{"id":12,"depth":95,"text":13},{"id":47,"depth":95,"text":48,"children":503},[504,505,506],{"id":52,"depth":101,"text":53},{"id":193,"depth":101,"text":194},{"id":214,"depth":101,"text":215},{"id":232,"depth":95,"text":233,"children":508},[509,510,511],{"id":236,"depth":101,"text":237},{"id":286,"depth":101,"text":287},{"id":297,"depth":101,"text":298},{"id":304,"depth":95,"text":305},{"id":341,"depth":95,"text":342},{"id":439,"depth":95,"text":440},{"id":487,"depth":95,"text":488},"Laravel","2025-04-20","The patterns, principles, and practices I use across 100+ production Laravel applications. Covers architecture, performance, security, and team collaboration.","md",false,{},"\u002Fblog\u002Flaravel-best-practices-2025",{"title":5,"description":518},"blog\u002Flaravel-best-practices-2025",[516,526,527,528,529],"PHP","Backend","Best Practices","Architecture","WWXiK2N5T1zkJgkC_ilhWCg9iOrN_9OG8MiWGuR_VOY",1782916136728]