Laravel has one of the richest ecosystems in web development — Eloquent, Artisan, Sanctum, Queues, Policies, Form Requests, Service Providers. That richness is exactly why AI-generated Laravel code goes wrong. Claude knows Laravel exists. What it doesn't know is which version you're on, whether you're using Sanctum or Passport, whether you're on Laravel 10 or 11, and what architectural patterns your team has committed to.
The result: code that runs, but doesn't fit. Controllers with business logic. Migrations without down(). Authorization checks embedded in view files. Facades in service classes that break unit tests.
A CLAUDE.md file locks in the context Claude needs to generate code that actually belongs in your Laravel project.
Here are 13 rules that eliminate the most common AI-generated Laravel mistakes.
Rule 1: Declare your Laravel and PHP versions explicitly
## Stack
- Laravel: 11.x (NOT 10.x — app.php structure, bootstrapping, and defaults differ)
- PHP: 8.3
- Database: MySQL 8.0
- Key packages: Sanctum 4.x, Pest 2.x, Livewire 3.x (if applicable)
Laravel 10 and Laravel 11 have meaningfully different project structures. The app/Http/Kernel.php middleware registration that worked in L10 doesn't exist in L11. The bootstrap/app.php API is completely different. Specify the version or Claude generates code for whichever version it's most familiar with.
Rule 2: Business logic belongs in Action classes or Services, not controllers
## Architecture
Controllers are thin HTTP adapters ONLY. They:
1. Validate input (via Form Request class)
2. Call one Action or Service class
3. Return a response
Business logic goes in app/Actions/ or app/Services/.
Never put database queries, calculations, or multi-step operations in controllers.
// CORRECT
class StoreOrderController extends Controller
{
public function __invoke(StoreOrderRequest $request, CreateOrder $action): JsonResponse
{
$order = $action->execute($request->validated());
return response()->json($order, 201);
}
}
Claude defaults to putting all logic inside controller methods. Specify the Action pattern or Service layer pattern you use, or you'll spend code review time extracting logic you didn't ask for.
Rule 3: All request validation goes in Form Request classes
## Validation
ALL input validation MUST use dedicated Form Request classes in app/Http/Requests/.
Never use $request->validate() inline in controllers.
// CORRECT — app/Http/Requests/StoreOrderRequest.php
class StoreOrderRequest extends FormRequest
{
public function rules(): array
{
return [
'product_id' => ['required', 'integer', 'exists:products,id'],
'quantity' => ['required', 'integer', 'min:1', 'max:100'],
];
}
}
// WRONG
public function store(Request $request)
{
$validated = $request->validate([...]); // don't do this
}
Form Requests are reusable, independently testable, and keep controllers thin. Without this rule, Claude puts validation anywhere it finds a controller method.
Rule 4: Authorization uses Policies — never embed checks in controllers or views
## Authorization
Use Laravel Policies for all authorization logic.
NEVER use if (auth()->user()->role === 'admin') in controllers or views.
NEVER embed Gate::allows() calls directly in Blade templates.
// CORRECT
class OrderPolicy
{
public function update(User $user, Order $order): bool
{
return $user->id === $order->user_id;
}
}
// In controller
$this->authorize('update', $order);
// WRONG — hardcoded role check in controller
if ($request->user()->role !== 'admin') {
abort(403);
}
Role-based checks scattered across controllers create security gaps and are impossible to audit. Policies centralize authorization logic in one testable place.
Rule 5: Migrations must always implement down()
## Migrations
Every migration MUST implement a complete down() method that fully reverses up().
If a migration adds a column, down() drops it.
If a migration creates a table, down() drops the table.
Irreversible migrations (data transformations) must be flagged with a comment.
// CORRECT
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropColumn('shipped_at');
});
}
Claude often generates migrations with empty down() methods or returns without content. This makes rollbacks impossible and creates problems in staging/CI environments.
Rule 6: Use Eloquent relationships — never manual joins in application code
## Eloquent
Use Eloquent relationships (hasMany, belongsTo, hasManyThrough, etc.) for all
related data access. Do NOT write raw JOIN queries in controllers or services.
Use eager loading (with()) to prevent N+1 queries.
// CORRECT
$orders = Order::with(['user', 'items.product'])->where('status', 'pending')->get();
// WRONG
$orders = DB::table('orders')
->join('users', 'orders.user_id', '=', 'users.id')
->select('orders.*', 'users.name')
->get();
Claude frequently generates raw query builder joins when Eloquent relationships exist. Specify this rule or you'll have inconsistent data access patterns throughout the codebase.
Rule 7: Never use Facades in injectable classes
## Facades vs Dependency Injection
Classes in app/Services/, app/Actions/, and app/Jobs/ MUST use constructor
injection — NOT Facades.
Facades are acceptable in config files, routes, and Blade templates only.
// CORRECT — testable, injectable
class OrderNotifier
{
public function __construct(private readonly Mailer $mailer) {}
public function notify(Order $order): void
{
$this->mailer->to($order->user)->send(new OrderConfirmation($order));
}
}
// WRONG — Facade in service class breaks unit testing
class OrderNotifier
{
public function notify(Order $order): void
{
Mail::to($order->user)->send(new OrderConfirmation($order));
}
}
Facade usage in service classes makes unit testing impossible without full framework bootstrapping. This is one of the most common architectural mistakes in AI-generated Laravel code.
Rule 8: Long-running operations go in queued Jobs — never block requests
## Queues
ANY operation that takes >200ms or involves external services (email, SMS, webhooks,
image processing, PDF generation) MUST be dispatched as a queued Job.
Never process these synchronously in controllers.
// CORRECT
ProcessOrderFulfillment::dispatch($order);
SendOrderConfirmationEmail::dispatch($order);
// WRONG
(new ProcessOrderFulfillment)->handle($order); // blocks the HTTP response
Without this rule, Claude generates synchronous external service calls inside controllers — email sending, webhook dispatching, file processing. These block responses, time out under load, and have no retry mechanism.
Rule 9: Environment values come from config files — never from env() directly
## Configuration
NEVER call env() inside application code (controllers, services, models).
env() only belongs in config/ files.
Application code reads from config() instead.
// CORRECT — in config/services.php
'stripe' => ['secret' => env('STRIPE_SECRET')],
// In application code
$secret = config('services.stripe.secret');
// WRONG — direct env() call in service class
$secret = env('STRIPE_SECRET');
Direct env() calls break when Laravel's config is cached (php artisan config:cache). Claude generates these without thinking about the caching layer.
Rule 10: Use Artisan commands for all CLI operations
## CLI Operations
Scheduled tasks, data imports, maintenance scripts, and batch operations
MUST be implemented as Artisan commands in app/Console/Commands/.
Never create standalone PHP scripts that bootstrap Laravel manually.
// CORRECT
php artisan orders:process-pending --dry-run
// Scheduled in routes/console.php (Laravel 11)
Schedule::command('orders:process-pending')->daily();
// WRONG
php scripts/process_orders.php // manual bootstrap = fragile
Ad-hoc scripts that bootstrap Laravel manually break when framework internals change. Artisan commands get proper DI, error handling, and scheduling integration automatically.
Rule 11: API resources for all JSON output — never ->toArray() or raw model responses
## API Responses
All API endpoints MUST return data through Laravel API Resource classes.
Never return $model->toArray(), $model->toJson(), or raw Eloquent collections.
// CORRECT
return new OrderResource($order);
return OrderResource::collection($orders);
// WRONG
return response()->json($order->toArray());
return response()->json($orders);
Raw model responses leak database column names, include sensitive fields, and can't be versioned. API Resources provide a stable, explicit transformation layer. Without this rule, Claude returns raw models from every controller.
Rule 12: Tests use database transactions — never manual teardown
## Testing
All tests that touch the database MUST use the RefreshDatabase or
DatabaseTransactions trait.
Never manually delete or truncate records in tearDown().
Use factories for all test data — never hardcode IDs or raw inserts.
use RefreshDatabase;
public function test_order_is_created(): void
{
$user = User::factory()->create();
$product = Product::factory()->create(['price' => 2700]);
$response = $this->actingAs($user)
->postJson('/api/orders', ['product_id' => $product->id, 'quantity' => 1]);
$response->assertCreated();
$this->assertDatabaseHas('orders', ['user_id' => $user->id]);
}
Claude generates tests with hardcoded IDs, raw DB inserts, and manual cleanup. These fail non-deterministically and create test pollution. Factories + RefreshDatabase make tests reproducible.
Rule 13: Observers for model event side effects — not lifecycle hooks in models
## Model Events
Side effects triggered by model events (sending emails, creating related records,
updating caches) MUST use Observer classes — not model boot() methods or
static::creating() closures inside the model.
// CORRECT — app/Observers/OrderObserver.php
class OrderObserver
{
public function created(Order $order): void
{
SendOrderConfirmationEmail::dispatch($order);
}
}
// Register in AppServiceProvider
Order::observe(OrderObserver::class);
// WRONG — business logic in model boot()
protected static function boot(): void
{
parent::boot();
static::created(function ($order) {
Mail::to($order->user)->send(new OrderConfirmation($order)); // facade + sync
});
}
Model lifecycle hooks embedded in boot() are invisible, hard to test, and mix concerns. Observers are explicit, registerable, and independently testable. Without this rule, Claude adds side effects directly into model boot methods.
Putting it together
Laravel's conventions are its strength and its trap. The framework has strong opinions about where things go — Form Requests, Policies, Observers, Jobs, API Resources — and Claude generates code that works without knowing which convention you've adopted.
A CLAUDE.md that specifies your Laravel version, your Action/Service pattern, your Facade rules, and your testing approach gives Claude the architectural context to generate code that fits your project's structure instead of inventing its own.
If you're using Claude Code or Cursor for a Laravel project, the full CLAUDE.md template — covering 24 other frameworks — is in the CLAUDE.md Rules Pack.
→ oliviacraftlat.gumroad.com/l/skdgt — $27, instant download
Top comments (0)