Skip to content

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
GETRead/api/users
POSTCreate/api/users
PUTUpdate (Replace)/api/users/1
PATCHUpdate (Modify)/api/users/1
DELETEDelete/api/users/1
OPTIONSCORS 预检/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,comments

HTTP 状态码使用

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=2

2. 速率限制

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 处理

参考链接