引言
API 是前后端与第三方的「公共契约」——设计得好,改起来毫不费劲;设计得差,每次迭代都在破坏别人。RESTful 的价值不是「标准答案」,而是用 HTTP 语义自描述资源操作。本文覆盖资源建模、状态码、认证、版本控制、契约驱动测试,给出 Laravel 落地写法与一套「先契约后实现」的工作流。
前置:/php-laravel-internals/(路由/中间件)、/php-security-hardening/(认证安全)。数据库见 /php-mysql-database/。
目录
- 1. REST 资源建模:名词而非动词
- 2. HTTP 动词与状态码语义
- 3. 响应结构约定:统一封装与错误
- 4. 认证方案:JWT、OAuth2 与 API Key
- 5. 速率限制与防滥用
- 6. 版本控制策略
- 7. OpenAPI:契约驱动开发
- 8. Laravel API 工程化
- 9. API 测试与契约测试
- 10. 速查表
- 延伸阅读
1. REST 资源建模:名词而非动词
REST 核心:把业务抽象成资源,用 HTTP 动词表达操作——URL 里是名词,不是动词:
❌ 动词式(RPC 味) ✅ 资源式(REST 味)
GET /getUsers GET /users
POST /createUser POST /users
POST /deleteUser?id=1 DELETE /users/1
POST /updateUser PATCH /users/1
POST /login POST /auth/token ← 认证是「换取凭证」,算资源操作
资源层级:
/users 用户集合
/users/1 单个用户
/users/1/orders 用户 1 的订单(子资源)
/orders/5/items 订单 5 的明细
子资源 vs 扁平:深嵌套过多(>2 层)就扁平化,用查询参数表达关联。
记忆:URL 描述「是什么资源」,动词描述「做什么」,查询参数描述「怎么筛选」。
2. HTTP 动词与状态码语义
| 动词 | 语义 | 幂等 | 返回 |
|---|---|---|---|
| GET | 读 | ✅ | 200 |
| POST | 创建(不确定 URI) | ❌ | 201 + Location |
| PUT | 整体替换 | ✅ | 200 |
| PATCH | 部分更新 | 可 | 200 |
| DELETE | 删除 | ✅ | 204 |
| HEAD | 读头 | ✅ | 200(无体) |
状态码语义(别乱用 200):
200 OK / 201 Created / 202 Accepted(异步)
204 No Content
400 参数错 / 401 未认证 / 403 无权限 / 404 不存在
409 冲突(如已存在)/ 422 校验失败(表单)
429 限流 / 5xx 服务端
反模式:全部返回 200 + {"code":0}——丢掉了 HTTP 的语义,客户端判断更累。
记忆:2xx 成功、4xx 客户端错、5xx 服务端错;422 专门给字段校验失败。
3. 响应结构约定:统一封装与错误
统一成功结构(分页必带 meta):
{
"data": [
{"id": 1, "name": "Alice"}
],
"meta": {
"page": 1, "per_page": 20, "total": 157, "last_page": 8
}
}
统一错误结构:
{
"error": {
"code": "validation_failed",
"message": "请求参数校验失败",
"details": {"email": ["邮箱格式不正确"]},
"traceId": "abc-123"
}
}
错误码约定:机器可判(code)+ 人可读(message)+ 细节(details)+ 排查(traceId)。
traceId 贯穿日志是排查命根子,见 [[observability]]。
4. 认证方案:JWT、OAuth2 与 API Key
JWT(无状态 Bearer Token):
// 生成(示例,生产用成熟库如 tymon/jwt-auth 或 Sanctum)
$payload = [
'sub' => $user->id,
'exp' => time() + 3600, // 1 小时
'iat' => time(),
];
$token = jwt_encode($payload, $secret, 'HS256');
// 校验中间件
// 解析 → 验签 → 查 sub → 注入当前用户
OAuth2(授权码流程,第三方登录):用户授权 → 换 token → 访问资源。适合「授权他人访问你的资源」。
API Key(服务间/简单):
// 头或查询参数传 key,查表校验
$key = $request->header('X-API-Key');
$client = ApiClient::where('key_hash', hash('sha256', $key))->first();
| 方案 | 适用 | 特点 |
|---|---|---|
| JWT | 自家 App/SPA | 无状态、可跨服务 |
| Sanctum(Laravel) | 自家 API | 内置、简单 |
| OAuth2 | 第三方授权 | 授权码 + 令牌交换 |
| API Key | 服务间 | 简单、需限流 |
Laravel 推荐:内置 Sanctum(token 认证)起步,复杂授权再上 OAuth。
5. 速率限制与防滥用
Laravel RateLimiter:
// 按 IP 限流
RateLimiter::for('api', fn($job) =>
Limit::perMinute(60)->by($job->user?->id ?: $job->ip()));
// 路由组应用
Route::middleware(['auth:sanctum', 'throttle:api'])->group(...);
自定义限制维度:
// 登录接口更严:按 IP + 邮箱
RateLimiter::for('login', fn($job) =>
Limit::perMinute(5)->by($job->ip() . '|' . $job->input('email')));
429 响应:返回 Retry-After 头,客户端可退避重试。
防滥用清单:限流(频控)、验证码(注册/登录)、账号锁定(多次失败)、WAF 层防护。
6. 版本控制策略
为什么要版本:契约变化(加必填字段、改语义、删资源)会破坏既有客户端。
三种主流策略:
| 策略 | 方式 | 优缺点 |
|---|---|---|
| URI 版本 | /api/v1/users | 显式、简单,膨胀 URL |
| 查询参数 | /users?version=1 | 不推荐(可缓存性问题) |
| Header 版本 | Accept: application/vnd.app.v1+json | 干净但隐藏 |
实践建议:
- 默认
/api/v1/,破坏性变更升v2(/api/v2/) - 向后兼容优先:加可选字段不升版本
- 废弃周期:标记
deprecated头 + 通知期 - URL 语义不破坏(改字段值格式也应升版本)
Route::prefix('v1')->group(function () {
Route::get('/users', ...);
});
Route::prefix('v2')->group(function () {
Route::get('/users', ...); // 新语义
});
记忆:加字段向后兼容,改语义就升版本;v1/v2 并行跑,给客户端留迁移时间。
7. OpenAPI:契约驱动开发
OpenAPI(Swagger):用一份 YAML/JSON 描述整个 API——文档、测试、客户端生成同源。
openapi: 3.0.0
info: { title: User API, version: "1.0.0" }
paths:
/users:
get:
summary: 用户列表
parameters:
- name: page
in: query
schema: { type: integer }
responses:
"200":
description: 成功
content:
application/json:
schema:
$ref: "#/components/schemas/UserList"
components:
schemas:
User:
type: object
properties:
id: { type: integer }
name: { type: string }
契约驱动工作流:
写 OpenAPI 契约 → 生成 mock/文档 → 前后端并行开发
→ 契约校验(请求/响应符合 schema)→ 交付
Laravel 配合:scramble 或 l5-swagger 从代码生成/校验契约;测试断言响应符合 schema。
记忆:契约先行 = 接口文档不再是「事后补」,mock 让前后端解耦并行。
8. Laravel API 工程化
API 资源(API Resource)——统一输出层,数据库结构不与响应耦合:
// app/Http/Resources/UserResource.php
class UserResource extends JsonResource {
public function toArray($request): array {
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'created_at' => $this->created_at->toISOString(),
];
}
}
// 控制器返回资源
Route::get('/users/{user}', fn(User $user) => new UserResource($user));
工程化清单:
| 项 | 做法 |
|---|---|
| 校验 | FormRequest(类型化校验) |
| 资源层 | API Resource 统一输出 |
| 错误 | ApiException 统一结构 |
| 认证 | Sanctum / JWT |
| 限流 | RateLimiter |
| 文档 | OpenAPI |
| 日志 | traceId + 请求上下文 |
// FormRequest 示例:校验与授权
class StoreUserRequest extends FormRequest {
public function authorize(): bool { return true; }
public function rules(): array {
return [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'unique:users'],
];
}
}
9. API 测试与契约测试
集成测试(Laravel Pest/PHPUnit):
test('用户列表返回分页结构', function () {
$response = $this->getJson('/api/v1/users?page=1');
$response->assertOk()
->assertJsonStructure([
'data' => [['id' => 'integer', 'name' => 'string']],
'meta' => ['total' => 'integer'],
]);
});
test('未认证访问返回 401', function () {
$this->getJson('/api/v1/users/me')->assertUnauthorized();
});
契约测试(防止前后端契约漂移):
// 用 OpenAPI schema 校验响应
$schema = loadSchema('components/schemas/User');
$response->assertValidAgainst($schema);
API 测试清单:正常路径、认证、校验失败、404/409、限流 429、分页边界、超时。
10. 速查表
| 需求 | 做法 |
|---|---|
| 资源建模 | URL 名词 + 动词 + 查询参数 |
| 状态码 | 语义化(201/204/400/401/403/404/409/422/429) |
| 响应 | 统一 data/meta + 错误结构 |
| 认证 | Sanctum / JWT / OAuth2 按需 |
| 限流 | RateLimiter + Retry-After |
| 版本 | /api/v1/,破坏性变更升版 |
| 文档 | OpenAPI 契约 |
| 校验 | FormRequest |
| 输出 | API Resource |
| 测试 | 集成 + 契约校验 |
一句话记忆:URL 名词、动词表操作、状态码说结果;认证 Sanctum 起步、限流必须有、破坏性改动升版本;契约先行,文档同源。
延伸阅读
- /php-laravel-internals/ — 路由与中间件管道
- /php-security-hardening/ — 认证与会话安全
- /php-testing-practice/ — API 集成测试
- /php-microservices-message-queue/ — API 背后的异步处理
- [[nodejs]] — 服务端 API 的跨语言对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。