4、请求与响应格式:JSON格式规范、请求头设计、响应结构设计
好,咱们今天聊点实在的。请求和响应格式,说白了就是你的API跟客户端之间怎么「说话」。我见过太多项目,接口写得乱七八糟,前端同事天天跑来问「这个字段是啥意思」「为什么返回格式不一样」。嗯,今天咱们就把这事彻底理清楚。
4.1 为什么非要用JSON?
你可能要问:XML不行吗?Form-Data不行吗?当然行。但说实话,在RESTful API的世界里,JSON已经是事实标准了。我个人习惯,只要不是历史遗留项目,一律用JSON。
为什么?三个字:轻、快、爽。
- 轻:JSON的语法比XML简洁太多,同样的数据,JSON体积能小30%-50%。
- 快:PHP的
json_encode()和json_decode()是C扩展实现的,性能极好。 - 爽:几乎所有语言都有原生支持,前端JavaScript直接
JSON.parse()就能用。
核心原则:请求体用JSON,响应体也用JSON。保持一致性,别搞混搭。
4.2 请求头设计:别小看这些「元数据」
请求头,很多人觉得不就是传个Token吗?其实远不止这些。我在项目中踩过一个坑:客户端传了Content-Type为 application/x-www-form-urlencoded,但服务端死活解析不到JSON数据。后来发现,是前端忘了设请求头。
下面是我常用的请求头规范:
| 请求头 | 必填 | 说明 | 示例值 |
|---|---|---|---|
Content-Type |
是 | 告诉服务端请求体的格式 | application/json |
Accept |
是 | 告诉服务端你期望的响应格式 | application/json |
Authorization |
是(需认证时) | Bearer Token 或 JWT | Bearer eyJhbGciOiJIUzI1NiIs... |
X-Request-Id |
推荐 | 用于链路追踪,方便排查问题 | 550e8400-e29b-41d4-a716-446655440000 |
X-Client-Version |
推荐 | 客户端版本号,用于兼容性判断 | 2.1.0 |
小技巧:我习惯在中间件里统一校验 Content-Type。如果客户端传的不是 application/json,直接返回 415 Unsupported Media Type。这样能避免很多奇奇怪怪的解析错误。
4.3 响应结构设计:让前端不再骂人
响应结构,这是整个API设计里最容易被忽视的地方。我曾经接手过一个项目,同一个接口,成功时返回 { "data": {...} },失败时返回 { "error": "xxx" }。前端每次都要写两套解析逻辑,你说累不累?
所以,我强烈建议:统一响应结构。不管成功还是失败,格式必须一致。
4.3.1 标准响应结构
{
"code": 0, // 业务状态码,0表示成功
"message": "success", // 提示信息
"data": { // 实际数据,失败时为null
"id": 123,
"name": "张三",
"email": "zhangsan@example.com"
},
"meta": { // 元数据,分页等信息放这里
"current_page": 1,
"per_page": 20,
"total": 100
},
"request_id": "550e8400-e29b-41d4-a716-446655440000" // 方便排查
}
你可能会问:为什么不用HTTP状态码直接表示业务状态?嗯,这个问题我当年也纠结过。后来发现,HTTP状态码只有几十个,但业务错误可能有几百种。比如「用户余额不足」和「用户被冻结」,都是400,但业务含义完全不同。所以,HTTP状态码管传输,业务状态码管逻辑,各司其职。
4.3.2 业务状态码设计
| 范围 | 含义 | 示例 |
|---|---|---|
| 0 | 成功 | 一切正常 |
| 1000-1999 | 通用错误 | 1001: 参数错误, 1002: 未授权 |
| 2000-2999 | 用户相关 | 2001: 用户不存在, 2002: 密码错误 |
| 3000-3999 | 业务相关 | 3001: 余额不足, 3002: 订单已取消 |
| 4000-4999 | 系统错误 | 4001: 数据库异常, 4002: 第三方服务超时 |
避坑指南:我曾经犯过一个错——把业务状态码和HTTP状态码混着用。比如用户未登录,我返回了401,但业务code却是0。前端一看code=0,以为成功了,直接渲染页面,结果一片空白。后来我定了个死规矩:HTTP状态码只反映传输层状态,业务状态码只反映业务层状态。两者互不干扰。
4.3.3 错误响应示例
// 参数校验失败
{
"code": 1001,
"message": "参数校验失败",
"data": {
"errors": {
"email": "邮箱格式不正确",
"age": "年龄必须在1-150之间"
}
},
"meta": null,
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
// 未授权
{
"code": 1002,
"message": "Token已过期,请重新登录",
"data": null,
"meta": null,
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
4.4 实战:PHP中如何统一处理响应
说了这么多理论,咱们来点实际的。我在项目中一般会封装一个 ApiResponse 类,所有控制器都通过它来返回数据。
<?php
namespace App\Http;
class ApiResponse
{
// 成功响应
public static function success($data = null, string $message = 'success', array $meta = [])
{
return response()->json([
'code' => 0,
'message' => $message,
'data' => $data,
'meta' => $meta,
'request_id' => request()->header('X-Request-Id', uniqid()),
]);
}
// 错误响应
public static function error(int $code, string $message, $data = null)
{
return response()->json([
'code' => $code,
'message' => $message,
'data' => $data,
'meta' => null,
'request_id' => request()->header('X-Request-Id', uniqid()),
]);
}
// 分页响应
public static function paginated($paginator, string $message = 'success')
{
return self::success(
data: $paginator->items(),
message: $message,
meta: [
'current_page' => $paginator->currentPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'last_page' => $paginator->lastPage(),
]
);
}
}
使用起来也很简单:
// 在控制器里
public function show($id)
{
$user = User::find($id);
if (!$user) {
return ApiResponse::error(2001, '用户不存在');
}
return ApiResponse::success($user);
}
个人经验:我建议在全局异常处理器里也统一捕获异常,自动转换成标准响应格式。这样即使代码里忘了处理异常,也不会返回乱七八糟的HTML或者堆栈信息给客户端。说白了,就是给API加一层「兜底」。
4.5 总结一下
请求和响应格式,看似简单,但决定了你的API好不好用。记住三点:
- 请求头要规范:Content-Type、Accept、Authorization 一个都不能少。
- 响应结构要统一:code、message、data、meta、request_id,固定这五个字段。
- 状态码要分离:HTTP状态码管传输,业务状态码管逻辑,别混在一起。
嗯,今天就聊到这儿。下一章咱们讲讲认证与授权,那可是个重头戏。