PHP RESTful

Resources, methods, status codes, and a framework-free minimal REST implementation.

Intermediate
Key PointsREST means nouns for resources and HTTP methods for actions.

Core conventions

MethodPathMeaningStatus
GET/users列表200
GET/users/1单个200 / 404
POST/users创建201 + Location
PUT/users/1整体替换200 / 204
PATCH/users/1部分更新200 / 204
DELETE/users/1删除204 / 404

No verbs in paths. /getUser?id=1 is RPC; /users/1 is REST.

A minimal implementation

PHP
<?php
declare(strict_types=1);

header("Content-Type: application/json; charset=utf-8");

$method = $_SERVER["REQUEST_METHOD"];
$path   = parse_url($_SERVER["REQUEST_URI"], PHP_URL_PATH);
$path   = rtrim($path, "/") ?: "/";

function json(array $data, int $status = 200): never {
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_UNICODE);
    exit;
}

$users = [
    1 => ["id" => 1, "name" => "Tom"],
    2 => ["id" => 2, "name" => "Amy"],
];

if ($path === "/users" && $method === "GET") {
    json(["items" => array_values($users)]);
}

if (preg_match("#^/users/(\d+)$#", $path, $m)) {
    $id = (int)$m[1];

    if (!isset($users[$id])) {
        json(["error" => "用户不存在"], 404);
    }

    if ($method === "GET") {
        json($users[$id]);
    }

    if ($method === "DELETE") {
        unset($users[$id]);
        http_response_code(204);
        exit;
    }
}

if ($path === "/users" && $method === "POST") {
    $body = json_decode(file_get_contents("php://input"), true) ?: [];
    $id = max(array_keys($users)) + 1;

    json(["id" => $id, "name" => $body["name"] ?? ""], 201);
}

json(["error" => "未找到路由"], 404);

Response for GET /users:

Output
{"items":[{"id":1,"name":"Tom"},{"id":2,"name":"Amy"}]}

Response for GET /users/9 (404):

Output
{"error":"用户不存在"}

Response for POST /users (201):

Output
{"id":3,"name":"Bob"}

Status codes

CodeMeaningWhen
200OK查询成功、更新成功
201Created创建成功,带 Location 头
204No Content删除或更新成功且无返回体
400Bad RequestJSON 格式错、参数缺失
401Unauthorized未登录或 token 无效
403Forbidden已登录但无权限
404Not Found资源不存在
405Method Not Allowed路径对但方法不对
422Unprocessable Entity校验失败
429Too Many Requests限流
500Internal Server Error未捕获异常
**Never return 200 with code: 500 in the body.** Status codes drive CDNs, gateways, monitoring and retries — burying them breaks all of it.

Routing

Real projects use regex or trie routers. A method+pattern table is clearest by hand.

PHP
<?php
$routes = [
    ["GET",    "#^/users$#",           "listUsers"],
    ["POST",   "#^/users$#",           "createUser"],
    ["GET",    "#^/users/(\d+)$#",     "getUser"],
    ["PATCH",  "#^/users/(\d+)$#",     "updateUser"],
    ["DELETE", "#^/users/(\d+)$#",     "deleteUser"],
];

$method = "GET";
$path   = "/users/7";

$allowed = [];
$matched = false;

foreach ($routes as [$m, $pattern, $handler]) {
    if (!preg_match($pattern, $path, $args)) {
        continue;
    }
    $allowed[] = $m;
    if ($m === $method) {
        $matched = true;
        echo "命中 $handler,参数 " . ($args[1] ?? "-") . "\n";
        break;
    }
}

if (!$matched) {
    echo $allowed ? "405 Allow: " . implode(",", $allowed) : "404 未找到";
}
echo "\n";
Output
命中 getUser,参数 7

A path match with the wrong method should be **405 with an Allow header**, not 404 — telling the client the URL is right but the method is not.

Versioning

  • Path version —— /api/v1/users, most explicit
  • Header version —— Accept: ...;version=1
  • Query version —— ?v=1, not recommended

Versioning exists so two versions can run side by side while clients migrate. Skip it if you do not need that.

Friends & Partners