/new-endpoint
Follow these steps in order. Do not skip steps or reorder them.
From laravel-api-tool-kitnpx claudepluginhub ahmedesa/laravel-api-tool-kitlaravel-api-tool-kit/workflows/Workflow: Add a New CRUD Endpoint
Follow these steps in order. Do not skip steps or reorder them.
Step 0 — Gather Requirements
Before creating any file, confirm:
- What is the resource name? (e.g.
Car) - What columns does it have and what are their types?
- Does it need soft deletes?
- Does it need file uploads?
- Which routes are needed? (all CRUD, or subset?)
- Is authentication required? Which guard?
Detect the project structure first
ls app/
- If you see
Domain/→ DDD structure — checkls app/Domain/to confirm folder naming conventions used in the project - If you see
Models/,Http/only → Standard Laravel structure
Path map — resolve once, use throughout all steps
| Component | Standard Laravel | DDD |
|---|---|---|
| Model | app/Models/Car.php | app/Domain/Car/Models/Car.php |
| Filter | app/Filters/CarFilters.php | app/Domain/Car/Filters/CarFilters.php |
| Enum | app/Enums/CarStatusEnum.php | app/Domain/Car/Enums/CarStatusEnum.php |
| Action | app/Actions/CreateCarAction.php | app/Domain/Car/Actions/CreateCarAction.php |
| Policy | app/Policies/CarPolicy.php | app/Domain/Car/Policies/CarPolicy.php |
| Form Request | app/Http/Requests/Car/ | app/Http/Requests/Application/Car/ (or Dashboard/Car/) |
| Resource | app/Http/Resources/Car/CarResource.php | app/Http/Resources/Application/Car/CarResource.php |
| Controller | app/Http/Controllers/CarController.php | app/Http/Controllers/API/Application/Car/CarController.php |
Note: Controllers, Requests, and Resources always stay in
app/Http/regardless of structure — only domain logic moves intoapp/Domain/.
Step 1 — Model
Follow rules/models.md for the correct structure.
- Add all columns to
$fillable - Add
$castsfor booleans, enums, and arrays - Add
SoftDeletesif needed - Add relationships
- If project uses ULID: add
HasUlidstrait +$keyType = 'string'+$incrementing = false - Do NOT bind
$default_filtersyet — do that after creating the Filter (Step 3)
File: app/Models/Car.php — DDD: app/Domain/Car/Models/Car.php
Step 2 — Migration
Create a standard Laravel migration:
Use the project's primary key convention from SKILL.md Project Defaults:
// ULID project
Schema::create('cars', function (Blueprint $table) {
$table->ulid('id')->primary();
$table->string('name');
$table->string('color');
$table->boolean('is_active')->default(true);
$table->foreignUlid('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
// Auto-increment project
Schema::create('cars', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('color');
$table->boolean('is_active')->default(true);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
Step 3 — Filter
Follow rules/filters.md for the correct structure.
- Populate
$allowedFilterswith columns clients can filter by equality - Populate
$allowedSortswith sortable columns (always includecreated_at) - Populate
$columnSearchif text search is needed - Add
$allowedIncludesfor relationships clients can eager-load - Add custom filter methods for non-trivial conditions
File: app/Filters/CarFilters.php — DDD: app/Domain/Car/Filters/CarFilters.php
Then go back to the Model and bind it:
protected string $default_filters = CarFilters::class;
Step 4 — Enum (if needed)
Follow rules/enums.md for the correct structure. Create one for any fixed-value column (status, type, etc.).
File: app/Enums/CarStatusEnum.php — DDD: app/Domain/Car/Enums/CarStatusEnum.php
Cast it in the Model:
protected $casts = [
'status' => CarStatusEnum::class,
];
Use it in validation:
'status' => ['required', Rule::in(CarStatusEnum::values())],
Step 5 — Form Requests
Follow rules/requests.md for the correct structure.
- Create request: required rules
- Update request: same rules but with
sometimesprefix for partial updates
Files:
Standard: app/Http/Requests/Car/CreateCarRequest.php
app/Http/Requests/Car/UpdateCarRequest.php
DDD: app/Http/Requests/Application/Car/CreateCarRequest.php
app/Http/Requests/Application/Car/UpdateCarRequest.php
(use Dashboard/Car/ for admin-facing requests)
Step 6 — Resource
Follow rules/resources.md for the correct structure.
- Add all scalar fields to the array
- Use
$this->whenLoaded('relation')for every relationship - Use
dateTimeFormat()for all timestamps - Never add raw DB queries inside
toArray()
File: app/Http/Resources/Car/CarResource.php — DDD: app/Http/Resources/Application/Car/CarResource.php
Step 7 — Policy (if authorization needed)
Follow rules/authorization.md. Create a Policy if any endpoint needs ownership or role checks.
Include all applicable methods: viewAny, view, create, update, delete.
File: app/Policies/CarPolicy.php — DDD: app/Domain/Car/Policies/CarPolicy.php
Register in AppServiceProvider or via automatic discovery (Laravel auto-discovers policies by convention).
Step 8 — Action (only if needed)
Follow rules/actions.md for the correct structure. Create one only if the operation:
- Has multiple distinct steps (even if all on the same model)
- Calls an external service (file upload, email, payment)
- Needs to be reused across multiple controllers
If it's a simple Model::create($data) — do it directly in the controller. No Action needed.
For external 3rd-party integrations (SMS, payment, etc.), see rules/services.md.
File: app/Actions/CreateCarAction.php — DDD: app/Domain/Car/Actions/CreateCarAction.php
Step 9 — Controller
Follow rules/controllers.md for the correct structure. Use the pattern with a constructor-injected Action/Service/Repository if you created one in Step 8.
- Add
$this->authorize()on methods that need policy checks - Apply middleware at the route level (not in the constructor — deprecated in Laravel 12)
File: app/Http/Controllers/CarController.php — DDD: app/Http/Controllers/API/Application/Car/CarController.php
Step 10 — Language File
Since all user-facing strings MUST use trans(), create translation keys before writing messages in the controller.
// lang/en/car.php
return [
'created' => 'Car created successfully.',
'updated' => 'Car updated successfully.',
'deleted' => 'Car deleted successfully.',
];
Step 11 — Route
Register the resource route in routes/api.php:
// Full CRUD
Route::apiResource('cars', CarController::class)->middleware('auth:sanctum');
// Or manual registration for partial CRUD
Route::middleware('auth:sanctum')->group(function () {
Route::get('cars', [CarController::class, 'index']);
Route::post('cars', [CarController::class, 'store'])->middleware('throttle:10,1');
Route::get('cars/{car}', [CarController::class, 'show']);
Route::put('cars/{car}', [CarController::class, 'update']);
Route::delete('cars/{car}', [CarController::class, 'destroy']);
});
Step 12 — Factory & Test (recommended)
Create a factory:
// database/factories/CarFactory.php
public function definition(): array
{
return [
'name' => fake()->word(),
'color' => fake()->safeColorName(),
'is_active' => true,
'user_id' => User::factory(),
];
}
Write a feature test covering index, store, show, update, destroy.
Checklist
Before marking the feature done, verify (paths per Step 0 path map):
-
$fillableis populated -
$castscovers all booleans and enums - Filter class has meaningful
$allowedFiltersand$allowedSorts - All controller methods use
$request->validated() - All controller messages use
trans() - All resource timestamps use
dateTimeFormat() - All resource relationships use
whenLoaded() - Routes have appropriate middleware (auth, throttle)
-
declare(strict_types=1)on every new file - Every method has parameter types and return types