Loading...
Loading...
Build production-grade Laravel REST APIs using opinionated architecture patterns with Laravel best practices. Use when building, scaffoling, or reviewing Laravel APIs with specifications for stateless design, versioned endpoints, invokable controllers, Form Request DTOs, Action classes, JWT authentication, and PSR-12 code quality standards. Triggers on "build a Laravel API", "create Laravel endpoints", "add API authentication", "review Laravel API code", "refactor Laravel API", or "improve Laravel code quality".
npx skill4agent add noartem/laravel-vue-skills laravel-apireferences/architecture.mdroutes/api/
routes.php # Main entry point, version grouping
tasks.php # All task routes, all versions
projects.php # All project routes, all versions
app/Http/
Controllers/{Resource}/V1/
StoreController.php # Always invokable
IndexController.php
ShowController.php
Requests/{Resource}/V1/
StoreTaskRequest.php # Validation + payload() method
Payloads/{Resource}/
StoreTaskPayload.php # Simple DTOs with toArray()
Responses/
JsonDataResponse.php # Implements Responsable
JsonErrorResponse.php
Middleware/
HttpSunset.php
app/Actions/{Resource}/
CreateTask.php # Single-purpose business logic
app/Services/ # Only when logic too complex for Actions
app/Models/
Task.php # HasUlids trait, simple data access<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Concerns\HasUlids;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
final class Task extends Model
{
use HasFactory;
use HasUlids;
protected $fillable = [
'title',
'description',
'status',
'project_id',
];
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
public function project(): BelongsTo
{
return $this->belongsTo(Project::class);
}
}routes/api/{resource}.phpuse App\Http\Controllers\Tasks\V1;
Route::middleware(['auth:api'])->group(function () {
Route::get('/tasks', V1\IndexController::class);
Route::post('/tasks', V1\StoreController::class);
Route::get('/tasks/{task}', V1\ShowController::class);
Route::patch('/tasks/{task}', V1\UpdateController::class);
Route::delete('/tasks/{task}', V1\DestroyController::class);
});routes/api/routes.phpRoute::prefix('v1')->group(function () {
require __DIR__ . '/tasks.php';
});app/Http/Payloads/{Resource}/{Operation}Payload.php<?php
declare(strict_types=1);
namespace App\Http\Payloads\Tasks;
final readonly class StoreTaskPayload
{
public function __construct(
public string $title,
public ?string $description,
public string $status,
public string $projectId,
) {}
public function toArray(): array
{
return [
'title' => $this->title,
'description' => $this->description,
'status' => $this->status,
'project_id' => $this->projectId,
];
}
}app/Http/Requests/{Resource}/V1/{Operation}Request.php<?php
declare(strict_types=1);
namespace App\Http\Requests\Tasks\V1;
use App\Http\Payloads\Tasks\StoreTaskPayload;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
final class StoreTaskRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'title' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string', 'max:1000'],
'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])],
'project_id' => ['required', 'string', 'exists:projects,id'],
];
}
public function payload(): StoreTaskPayload
{
return new StoreTaskPayload(
title: $this->string('title')->toString(),
description: $this->string('description')->toString(),
status: $this->string('status')->toString(),
projectId: $this->string('project_id')->toString(),
);
}
}app/Actions/{Resource}/{Operation}.php<?php
declare(strict_types=1);
namespace App\Actions\Tasks;
use App\Http\Payloads\Tasks\StoreTaskPayload;
use App\Models\Task;
final readonly class CreateTask
{
public function handle(StoreTaskPayload $payload): Task
{
return Task::create($payload->toArray());
}
}app/Http/Controllers/{Resource}/V1/{Operation}Controller.php<?php
declare(strict_types=1);
namespace App\Http\Controllers\Tasks\V1;
use App\Actions\Tasks\CreateTask;
use App\Http\Requests\Tasks\V1\StoreTaskRequest;
use App\Http\Responses\JsonDataResponse;
use Illuminate\Http\JsonResponse;
final readonly class StoreController
{
public function __construct(
private CreateTask $createTask,
) {}
public function __invoke(StoreTaskRequest $request): JsonResponse
{
$task = $this->createTask->handle(
payload: $request->payload(),
);
return new JsonDataResponse(
data: $task,
status: 201,
);
}
}{
"data": {...},
"meta": {...}
}{
"type": "about:blank",
"title": "Validation Failed",
"status": 422,
"detail": "The given data was invalid",
"errors": {...}
}use Spatie\QueryBuilder\QueryBuilder;
$tasks = QueryBuilder::for(Task::class)
->allowedFilters(['status', 'priority'])
->allowedSorts(['created_at', 'due_date'])
->allowedIncludes(['project', 'assignee'])
->paginate();App\Http\Controllers\Tasks\V2\Route::middleware(['auth:api', 'http.sunset:2025-12-31'])->group(function () {
// V1 routes
});composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secretconfig/auth.php'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],app/Providers/AppServiceProvider.phpuse Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::shouldBeStrict(); // Prevent N+1 queries
}app/Http/Kernel.phpprotected $middlewareAliases = [
'http.sunset' => \App\Http\Middleware\HttpSunset::class,
];// ❌ Avoid: Nested ternary
$status = $task->completed_at
? ($task->verified ? 'verified' : 'completed')
: ($task->started_at ? 'in_progress' : 'pending');
// ✅ Prefer: Match expression
$status = match (true) {
$task->completed_at && $task->verified => 'verified',
$task->completed_at => 'completed',
$task->started_at => 'in_progress',
default => 'pending',
};assets/templates/