关于 laravel-api
laravel-api 是用于构建 Laravel REST API 的开发技能,面向实现新 API 或审查现有代码的开发者。它强调无状态、按资源组织和清晰分层,并提供端点结构、编码规范及脚手架模板。
主要功能
- 01按资源组织路由和控制器
- 02采用无状态、分层的架构原则
- 03提供 CRUD 端点构建流程
- 04规范 API 版本管理与响应格式
- 05提供组件模板和代码审查准则
适用场景
- 01为 Laravel 项目搭建 REST API
- 02为资源添加版本化 CRUD 端点
- 03重构或审查 API 代码的一致性
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ juststeveking/ laravel-api-skill --skill laravel-api安装后在 Claude Code 中输入/laravel-api确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
assets/templates/Action.phpController.phpFormRequest.phpModel.phpPayload.phpreferences/architecture.mdcode-examples.mdcode-quality.md详细介绍
Laravel API - Steve 的架构
使用简洁、无状态、按资源划分的架构构建 Laravel REST API。
快速开始
用户请求构建 Laravel API 时,遵循以下工作流:
- 了解需求 - 有哪些资源?需要哪些操作?是否需要身份验证?
- 初始化项目结构 - 配置路由,移除前端冗余内容
- 构建第一个资源 - 完成 CRUD,确立模式
- 添加身份验证 - 使用 PHP Open Source Saver 实现 JWT
- 迭代其余资源 - 遵循已确立的模式
核心架构原则
阅读 references/architecture.md 了解详情。主要原则:
- 设计为无状态 - 不依赖隐藏内容,数据流明确
- 边界优先 - 清晰划分 HTTP、业务逻辑和数据层
- 按资源划分 - 按资源组织路由和控制器
- 严格遵守版本规范 - 基于命名空间进行版本控制,使用 HTTP Sunset 标头
代码质量标准
所有代码都必须遵循 Laravel 最佳实践和 PSR-12 标准:
- 保留功能 - 重构只改变代码的工作方式,绝不改变代码的功能
- 明确优于隐式 - 优先编写清晰易读的代码,而不是取巧的简写
- 类型声明 - 方法始终声明返回类型,并在适用时声明参数类型
- 避免嵌套三元运算符 - 使用 match 表达式、switch 或 if/else 提高清晰度
- 命名一致 - 严格遵循 PSR-12 和 Laravel 约定
- 恰当使用命名空间 - 合理组织导入项,使用完整类型提示
审查或重构代码时:
- 优先考虑清晰度和可维护性,而非取巧
- 将复杂的嵌套逻辑简化为易读的结构
- 将魔法值提取为具名常量或配置项
- 在保持行为完全一致的同时,去除不必要的复杂性
项目结构
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
构建新的资源端点
第 1 步:模型
始终使用 ULID。保持模型简洁——仅负责数据访问。
<?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);
}
}
第 2 步:路由
在 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);
});
在 routes/api/routes.php 中引入:
Route::prefix('v1')->group(function () {
require __DIR__ . '/tasks.php';
});
第 3 步:DTO(负载)
在 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,
];
}
}
第 4 步:表单请求
在 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(),
);
}
}
第 5 步:Action
在 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());
}
}
第 6 步:控制器
在 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": {...}
}
错误(Problem+JSON):
{
"type": "about:blank",
"title": "Validation Failed",
"status": 422,
"detail": "The given data was invalid",
"errors": {...}
}
查询构建
使用 Spatie Query Builder 进行筛选、排序和包含关联数据:
use Spatie\QueryBuilder\QueryBuilder;
$tasks = QueryBuilder::for(Task::class)
->allowedFilters(['status', 'priority'])
->allowedSorts(['created_at', 'due_date'])
->allowedIncludes(['project', 'assignee'])
->paginate();
端点版本控制
创建 V2 时:
- 创建 V2 命名空间:
App\Http\Controllers\Tasks\V2\ - 在资源文件中添加 V2 路由组
- 为 V1 路由添加 Sunset 中间件:
Route::middleware(['auth:api', 'http.sunset:2025-12-31'])->group(function () {
// V1 routes
});
身份验证设置
使用 PHP Open Source Saver JWT 包:
composer require php-open-source-saver/jwt-auth
php artisan vendor:publish --provider="PHPOpenSourceSaver\JWTAuth\Providers\LaravelServiceProvider"
php artisan jwt:secret
在 config/auth.php 中配置:
'guards' => [
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
必要设置
添加到 app/Providers/AppServiceProvider.php:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::shouldBeStrict(); // Prevent N+1 queries
}
在 app/Http/Kernel.php 中注册 HttpSunset 中间件:
protected $middlewareAliases = [
'http.sunset' => \App\Http\Middleware\HttpSunset::class,
];
应避免的反模式
- 使用自增 ID 而不是 ULID
- 在模型中编写业务逻辑
- 一个控制器中包含多个 Action
- 在控制器或 Action 中直接访问请求数据
- 隐式查询作用域
- 可以用 Action 实现时却使用服务类
- 不进行版本控制就引入破坏性变更
- 响应格式不一致
- 使用嵌套三元运算符(改用 match 表达式)
- 方法和参数缺少类型声明
- 过度压缩的“巧妙”代码牺牲了可读性
代码审查与重构
审查或重构 Laravel API 代码时,应用以下原则:
简化检查清单
- 保留功能 - 确保重构不会改变行为
- 检查类型安全 - 补充缺失的返回类型和参数类型
- 简化逻辑 - 将嵌套三元表达式替换为 match 表达式
- 提取复杂逻辑 - 将复杂条件移入命名方法
- 验证规范 - 确保符合 PSR-12,并包含 declare(strict_types=1)
- 改进命名 - 使用能体现意图的描述性名称
Match 表达式模式
使用 match 替换嵌套三元表达式,以提升清晰度:
// ❌ 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',
};
参考资料
- architecture.md - 全面的架构模式和原则
- code-examples.md - 每个组件的完整可运行示例
- code-quality.md - Laravel 最佳实践、重构模式和 PSR-12 规范
模板
assets/templates/ 中的模板文件可用于快速搭建:
- Controller.php
- FormRequest.php
- Payload.php
- Action.php
- Model.php