Windsurf Rules for Laravel Projects
Windsurf.windsurfrulesCoding
Laravel conventions that keep controllers thin: Form Requests for all validation, no Eloquent queries outside models and actions, queued jobs for anything slow, Pest for tests.
Use when: Use in a Laravel 11+ application when you want Windsurf to follow framework idioms instead of writing raw queries in controllers or validating inline.
Get this skill
Add the skill file with the CLI, by hand, or over MCP.Save this to your project or home directory so Windsurf can load it.
npx goodprompts add windsurf-laravel-conventionsSaves to
./.windsurfrulesnpx goodprompts add windsurf-laravel-conventionsSaves to
./.windsurfrulesSkill file
.windsurfrules2,654 characters# Laravel Project Rules
## Eloquent discipline
- No Eloquent or query-builder calls in controllers, commands, or Blade views. Queries belong in the model (scopes), a dedicated Action class, or a query class under `app/Queries/`.
- Always eager-load relations you iterate over. If a loop touches `$model->relation`, the calling query needs `with()`. Treat any new N+1 as a bug, and keep `Model::preventLazyLoading()` enabled in `AppServiceProvider`.
- `$fillable` is required on every model; never use `$guarded = []`.
- Use database transactions (`DB::transaction`) around any action that writes to more than one table.
- Casts go in the `casts()` method; never store JSON in a string column without an `array`/`AsCollection` cast.
- Never call `Model::all()` on user-facing paths — paginate or chunk.
## HTTP layer
- Every POST/PUT/PATCH/DELETE endpoint validates via a Form Request class. Inline `$request->validate()` is forbidden.
- Authorization lives in the Form Request's `authorize()` or a Policy — never `if ($user->id !== ...)` checks in controllers.
- Controllers contain at most: resolve input, call one action, return a response. If a controller method exceeds ~15 lines, extract an Action.
- API responses go through `JsonResource` classes; never return raw models from API controllers.
## Jobs and queues
- Anything that sends mail, calls an external API, or takes longer than ~100ms goes in a queued job — never inline in the request cycle.
- Every job declares explicit `$tries` and `$backoff`, and implements `failed()` to log context.
- Jobs must be idempotent: guard with `WithoutOverlapping` or a unique key, and assume the queue may deliver twice.
- Pass model IDs to jobs, then refetch inside `handle()` — never serialize whole loaded relations.
- New jobs are dispatched via `dispatch()` from an Action, not from controllers directly.
## Pest tests
- All new tests use Pest syntax (`it(...)`, `expect(...)`), not PHPUnit classes.
- Feature tests hit real routes with `RefreshDatabase`; do not mock Eloquent.
- Every Form Request gets a test for one valid payload and each validation rule that can fail.
- Use factories for all test data; never insert via raw arrays or seeders inside tests.
- Queued side effects are asserted with `Queue::fake()` / `Mail::fake()`, not by running the job inline.
## Workflow
- Run `./vendor/bin/pint` and `php artisan test` before suggesting a commit.
- New migrations are additive in production paths — never edit a migration that has already been merged; add a new one.
- Config values are read via `config()` from a config file backed by `env()`; never call `env()` outside `config/`.