RESTful API 设计
REST(Representational State Transfer)是一种 API 设计架构风格,通过 HTTP 方法(GET/POST/PUT/DELETE)操作资源(URL 路径),实现清晰、一致的接口设计。本节讲解 RESTful API 的核心设计原则、URL 命名规范、HTTP 状态码使用和 PHP 中的 RESTful 路由实现。
前置知识
阅读本节前,建议先了解:HTTP 协议概览、GET 与 POST 请求
基础概念
REST 核心原则
| 原则 | 说明 | 示例 |
|---|---|---|
| 资源导向 | URL 代表资源(名词) | /users 而非 /getUsers |
| HTTP 语义化 | HTTP 方法表达操作 | POST 创建、GET 获取、PUT 更新、DELETE 删除 |
| 无状态 | 每个请求包含所有必要信息 | 不依赖 Session,使用 Token |
| 统一接口 | 一致的 URL 结构和响应格式 | 所有资源遵循相同模式 |
| 分层系统 | 客户端不知道直接连接的服务器 | 通过 Nginx/负载均衡器 |
| 可缓存 | 响应明确标识是否可缓存 | Cache-Control 头 |
HTTP 方法与 CRUD 映射
| HTTP 方法 | CRUD 操作 | 幂等 | 安全 | 典型 URL |
|---|---|---|---|---|
| GET | Read | 是 | 是 | /api/users |
| POST | Create | 否 | 否 | /api/users |
| PUT | Update (Replace) | 是 | 否 | /api/users/1 |
| PATCH | Update (Modify) | 否 | 否 | /api/users/1 |
| DELETE | Delete | 是 | 否 | /api/users/1 |
| OPTIONS | CORS 预检 | 是 | 是 | /api/users |
| HEAD | 获取头信息 | 是 | 是 | /api/users |
URL 设计规范
资源命名
# === 好的 URL 设计 ===
GET /api/users # 获取用户列表
GET /api/users/1 # 获取单个用户
POST /api/users # 创建用户
PUT /api/users/1 # 替换用户(全量)
PATCH /api/users/1 # 修改用户(部分)
DELETE /api/users/1 # 删除用户
GET /api/users/1/orders # 获取用户的订单
GET /api/users/1/posts?status=published # 带过滤条件
# === 错误的 URL 设计 ===
GET /api/getUsers # 使用动词
GET /api/user_list # 下划线命名
GET /api/User # 大写
POST /api/createUser # 使用动词
GET /api/users/1/ # 末尾斜杠(不推荐)
GET /api/users?method=create # GET 中执行创建命名规范
# 使用小写字母
/api/users
/api/user-profiles
# 使用连字符(-),不用下划线(_)
/api/user-profiles # 好
/api/user_profiles # 不推荐
# 使用复数名词
/api/users # 好
/api/user # 不推荐(单数)
# 资源嵌套不超过 2 层
/api/users/1/orders # 好
/api/users/1/orders/2/items # 过深,简化为 /api/order-items/2
# 过滤、排序、分页使用查询参数
GET /api/users?status=active&sort=-created_at&page=1&per_page=20查询参数约定
php
<?php
declare(strict_types=1);
// === 分页 ===
GET /api/users?page=2&per_page=20
// === 排序 ===
GET /api/users?sort=created_at&order=desc
GET /api/users?sort=-created_at # - 前缀表示降序
GET /api/users?sort=created_at,-name # 多字段排序
// === 过滤 ===
GET /api/users?status=active&role=admin
GET /api/users?created_at_from=2024-01-01&created_at_to=2024-12-31
GET /api/users?search=alice&fields=id,name,email
// === 包含关联资源 ===
GET /api/users/1?include=orders,profile
GET /api/posts?include=author,commentsHTTP 状态码使用
PHP 中统一的状态码设置
php
<?php
declare(strict_types=1);
/**
* API 状态码使用规范
*/
// === 2xx 成功 ===
http_response_code(200); // OK - 获取/更新成功
http_response_code(201); // Created - 创建成功
http_response_code(204); // No Content - 删除成功
// === 3xx 重定向 ===
http_response_code(301); // Moved Permanently
http_response_code(304); // Not Modified - 缓存有效
// === 4xx 客户端错误 ===
http_response_code(400); // Bad Request - 参数错误
http_response_code(401); // Unauthorized - 未认证
http_response_code(403); // Forbidden - 无权限
http_response_code(404); // Not Found - 资源不存在
http_response_code(405); // Method Not Allowed
http_response_code(409); // Conflict - 资源冲突
http_response_code(415); // Unsupported Media Type
http_response_code(422); // Unprocessable Entity - 验证失败
http_response_code(429); // Too Many Requests - 速率限制
// === 5xx 服务端错误 ===
http_response_code(500); // Internal Server Error
http_response_code(503); // Service Unavailable实战示例:PHP RESTful 路由器
php
<?php
declare(strict_types=1);
/**
* 简洁的 RESTful 路由器
*/
class Router
{
private array $routes = [];
/**
* 注册路由
*/
public function add(string $method, string $pattern, callable $handler): self
{
// 将路由模式转换为正则表达式
$regex = preg_replace('/\{(\w+)\}/', '(?P<$1>[^/]+)', $pattern);
$regex = '~^' . $regex . '$~';
$this->routes[] = [
'method' => strtoupper($method),
'regex' => $regex,
'handler' => $handler,
'pattern' => $pattern,
];
return $this;
}
public function get(string $pattern, callable $handler): self
{
return $this->add('GET', $pattern, $handler);
}
public function post(string $pattern, callable $handler): self
{
return $this->add('POST', $pattern, $handler);
}
public function put(string $pattern, callable $handler): self
{
return $this->add('PUT', $pattern, $handler);
}
public function patch(string $pattern, callable $handler): self
{
return $this->add('PATCH', $pattern, $handler);
}
public function delete(string $pattern, callable $handler): self
{
return $this->add('DELETE', $pattern, $handler);
}
/**
* 分发请求
*/
public function dispatch(): mixed
{
$method = $_SERVER['REQUEST_METHOD'];
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
foreach ($this->routes as $route) {
if ($route['method'] !== $method) {
continue;
}
if (preg_match($route['regex'], $uri, $matches)) {
// 提取路径参数
$params = array_filter($matches, 'is_string', ARRAY_FILTER_USE_KEY);
return call_user_func_array($route['handler'], $params);
}
}
// 未匹配路由
http_response_code(404);
return json_encode(['error' => 'Not Found']);
}
}
// === 定义路由 ===
$router = new Router();
$router->get('/api/users', function (): void {
$page = filter_input(INPUT_GET, 'page', FILTER_VALIDATE_INT, ['options' => ['default' => 1]]);
$perPage = filter_input(INPUT_GET, 'per_page', FILTER_VALIDATE_INT, ['options' => ['default' => 20]]);
// 查询数据库...
header('Content-Type: application/json');
echo json_encode(['data' => [], 'page' => $page, 'per_page' => $perPage]);
});
$router->post('/api/users', function (): void {
$data = json_decode(file_get_contents('php://input'), true);
// 验证并创建用户...
header('Content-Type: application/json');
http_response_code(201);
echo json_encode(['data' => ['id' => 1, 'name' => 'Alice']]);
});
$router->get('/api/users/{id}', function (string $id): void {
$userId = filter_var($id, FILTER_VALIDATE_INT);
if ($userId === false) {
http_response_code(400);
echo json_encode(['error' => 'Invalid ID']);
return;
}
// 查询用户...
echo json_encode(['data' => ['id' => $userId, 'name' => 'Alice']]);
});
$router->put('/api/users/{id}', function (string $id): void {
$data = json_decode(file_get_contents('php://input'), true);
// 全量更新用户...
echo json_encode(['data' => ['id' => $id, 'updated' => true]]);
});
$router->patch('/api/users/{id}', function (string $id): void {
$data = json_decode(file_get_contents('php://input'), true);
// 部分更新用户...
echo json_encode(['data' => ['id' => $id, 'updated' => true]]);
});
$router->delete('/api/users/{id}', function (string $id): void {
// 删除用户...
http_response_code(204);
});
// 处理 CORS 预检
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 86400');
http_response_code(204);
exit;
}
// 设置安全头
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
// 分发请求
$router->dispatch();统一响应格式
php
<?php
declare(strict_types=1);
class ApiResponse
{
/**
* 成功响应
*/
public static function success(mixed $data = null, string $message = 'success', int $code = 200): never
{
http_response_code($code);
header('Content-Type: application/json');
echo json_encode([
'code' => $code,
'message' => $message,
'data' => $data,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
/**
* 列表响应(带分页)
*/
public static function list(array $items, int $total, int $page, int $perPage): never
{
self::success([
'items' => $items,
'pagination' => [
'total' => $total,
'page' => $page,
'per_page' => $perPage,
'last_page' => (int) ceil($total / max(1, $perPage)),
],
]);
}
/**
* 创建成功响应
*/
public static function created(mixed $data = null): never
{
self::success($data, 'created', 201);
}
/**
* 无内容响应
*/
public static function noContent(): never
{
http_response_code(204);
exit;
}
/**
* 错误响应
*/
public static function error(string $message, int $code = 400, ?array $errors = null): never
{
http_response_code($code);
header('Content-Type: application/json');
$response = [
'code' => $code,
'message' => $message,
];
if ($errors !== null) {
$response['errors'] = $errors;
}
echo json_encode($response, JSON_UNESCAPED_UNICODE);
exit;
}
/**
* 验证失败响应
*/
public static function validationError(array $errors): never
{
self::error('Validation failed', 422, $errors);
}
}注意事项
1. 版本管理
# URL 路径版本化(推荐)
/api/v1/users
/api/v2/users
# Header 版本化
Accept: application/vnd.myapi.v2+json
# 查询参数版本化
/api/users?version=22. 速率限制
php
<?php
declare(strict_types=1);
// 简单的文件速率限制
function rateLimit(string $key, int $maxRequests = 100, int $windowSeconds = 60): bool
{
$cacheFile = sys_get_temp_dir() . '/rate_' . md5($key);
$data = file_exists($cacheFile) ? json_decode(file_get_contents($cacheFile), true) : null;
$now = time();
if ($data === null || ($now - $data['start']) >= $windowSeconds) {
$data = ['count' => 1, 'start' => $now];
} else {
$data['count']++;
}
file_put_contents($cacheFile, json_encode($data));
if ($data['count'] > $maxRequests) {
http_response_code(429);
header('Retry-After: ' . ($windowSeconds - ($now - $data['start'])));
echo json_encode(['error' => 'Too many requests']);
exit;
}
return true;
}
// 使用
rateLimit($_SERVER['REMOTE_ADDR'], 100, 60);最佳实践
1. RESTful API 设计清单
- [x] 使用名词复数作为资源路径
- [x] HTTP 方法对应 CRUD 操作
- [x] 使用正确的 HTTP 状态码
- [x] 统一的响应格式
- [x] 分页使用 page/per_page
- [x] 过滤使用查询参数
- [x] 支持字段选择(fields 参数)
- [x] 版本管理(/api/v1/)
- [x] CORS 支持
- [x] 速率限制
- [x] 错误响应统一格式下一节
继续学习:JSON 处理