NYC
skills/smithery/ai/laravel-api

laravel-api

SKILL.md

Laravel API - Steve's Architecture

Build Laravel REST APIs with clean, stateless, resource-scoped architecture.

Quick Start

When user requests a Laravel API, follow this workflow:

  1. Understand requirements - What resources? What operations? Authentication needed?
  2. Initialize project structure - Set up routing, remove frontend bloat
  3. Build first resource - Complete CRUD to establish pattern
  4. Add authentication - JWT via PHP Open Source Saver
  5. Iterate on remaining resources - Follow established pattern

Core Architecture Principles

Read references/architecture.md for comprehensive details. Key principles:

  1. Stateless by design - No hidden dependencies, explicit data flow
  2. Boundary-first - Clear separation of HTTP, business logic, data layers
  3. Resource-scoped - Routes, controllers organized by resource
  4. Version discipline - Namespace-based versioning, HTTP Sunset headers

Code Quality Standards

All code must follow Laravel best practices and PSR-12 standards:

  1. Preserve Functionality - Refactorings change HOW code works, never WHAT it does
  2. Explicit Over Implicit - Prefer clear, readable code over clever shortcuts
  3. Type Declarations - Always use return types on methods, parameter types where beneficial
  4. Avoid Nested Ternaries - Use match expressions, switch, or if/else for clarity
  5. Consistent Naming - Follow PSR-12 and Laravel conventions strictly
  6. Proper Namespacing - Organize imports logically, use full type hints

When reviewing or refactoring code:

  • Focus on clarity and maintainability over cleverness
  • Simplify complex nested logic into readable structures
  • Extract magic values into named constants or config
  • Remove unnecessary complexity while preserving exact behavior

Project Structure

routes/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

Building a New Resource Endpoint

Step 1: Model

Always use ULIDs. Keep models simple - data access only.

<?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);
    }
}

Step 2: Routes

Create resource route file at routes/api/{resource}.php:

use 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);
});

Include in routes/api/routes.php:

Route::prefix('v1')->group(function () {
    require __DIR__ . '/tasks.php';
});

Step 3: DTO (Payload)

Create at 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,
        ];
    }
}

Step 4: Form Request

Create at 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(),
        );
    }
}

Step 5: Action

Create at 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());
    }
}

Step 6: Controller

Create invokable controller at 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,
        );
    }
}

Response Format

Standard format for all responses:

Success:

{
    "data": {...},
    "meta": {...}
}

Error (Problem+JSON):

{
    "type": "about:blank",
    "title": "Validation Failed",
    "status": 422,
    "detail": "The given data was invalid",
    "errors": {...}
}

Query Building

Use Spatie Query Builder for filtering, sorting, includes:

use Spatie\QueryBuilder\QueryBuilder;

$tasks = QueryBuilder::for(Task::class)
    ->allowedFilters(['status', 'priority'])
    ->allowedSorts(['created_at', 'due_date'])
    ->allowedIncludes(['project', 'assignee'])
    ->paginate();

Versioning Endpoints

When creating V2:

  1. Create V2 namespace: App\Http\Controllers\Tasks\V2\
  2. Add V2 route group in resource file
  3. Add Sunset middleware to V1 routes:
Route::middleware(['auth:api', 'http.sunset:2025-12-31'])->group(function () {
    // V1 routes
});

Authentication Setup

Use PHP Open Source Saver JWT package:

composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret

Configure in config/auth.php:

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

Essential Setup

Add to app/Providers/AppServiceProvider.php:

use Illuminate\Database\Eloquent\Model;

public function boot(): void
{
    Model::shouldBeStrict(); // Prevent N+1 queries
}

Register HttpSunset middleware in app/Http/Kernel.php:

protected $middlewareAliases = [
    'http.sunset' => \App\Http\Middleware\HttpSunset::class,
];

Anti-Patterns to Avoid

  • Using auto-increment IDs instead of ULIDs
  • Business logic in models
  • Multiple actions per controller
  • Accessing request data directly in controllers/actions
  • Hidden query scopes
  • Service classes when an Action would suffice
  • Breaking changes without versioning
  • Inconsistent response formats
  • Nested ternary operators (use match expressions instead)
  • Missing type declarations on methods and parameters
  • Overly compact "clever" code that sacrifices readability

Code Review & Refactoring

When reviewing or refactoring Laravel API code, apply these principles:

Simplification Checklist

  1. Preserve Functionality - Ensure refactorings don't change behavior
  2. Check Type Safety - Add missing return types and parameter types
  3. Simplify Logic - Replace nested ternaries with match expressions
  4. Extract Complexity - Move complex conditions into named methods
  5. Verify Standards - Ensure PSR-12 compliance with declare(strict_types=1)
  6. Improve Naming - Use descriptive names that reveal intent

Match Expression Pattern

Replace nested ternaries with match for clarity:

// ❌ 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',
};

References

  • architecture.md - Comprehensive architectural patterns and principles
  • code-examples.md - Complete working examples for every component
  • code-quality.md - Laravel best practices, refactoring patterns, and PSR-12 standards

Templates

Template files in assets/templates/ for quick scaffolding:

  • Controller.php
  • FormRequest.php
  • Payload.php
  • Action.php
  • Model.php
Weekly Installs
2
Repository
smithery/ai
First Seen
4 days ago
Installed on
opencode2