理解HTTP协议与RESTful架构
说实话,很多做接口对接的工程师上来就怼代码,结果出了问题连是GET还是POST都分不清。我个人习惯,在动手写任何API之前,先把HTTP协议和RESTful设计原则梳理一遍。你想想看,连传输的基础都不牢,后面解耦、缓存、安全全是空中楼阁。
HTTP协议基础
HTTP说白了就是客户端和服务端之间的一种约定——你要什么、我给你什么、给不了是什么原因。这里面最核心的就三样:方法、状态码、请求头。
常用HTTP方法
| 方法 | 语义 | 幂等 | 典型场景 |
|---|---|---|---|
| GET | 获取资源 | 是 | 查询列表、详情 |
| POST | 创建资源 | 否 | 新增订单、注册 |
| PUT | 全量更新 | 是 | 更新用户信息 |
| PATCH | 部分更新 | 否 | 修改手机号 |
| DELETE | 删除资源 | 是 | 下架商品 |
我在项目中遇到过有人用POST去做查询,理由是“参数太长怕被浏览器截断”。嗯,确实有这种极端情况,但更合理的做法是用GET配合请求体?不,GET没有请求体。正确的做法是用POST加上合理的缓存策略,或者干脆用GraphQL。但那是后话了。
状态码分类
状态码是服务端给客户的“一句话回复”。记住三个范围就够了:
- 2xx —— 全对。200表示一切正常,201表示创建成功。
- 4xx —— 你错了。400请求格式不对,401没登录,403没权限,404资源不存在。
- 5xx —— 我错了。500服务器内部错误,503服务暂时不可用。
技巧:曾经有个同事把密码错误返回500,前端直接弹窗“服务器故障”。这锅甩得冤枉啊。正确的做法是返回401,并带上明确错误码。
请求头:不只看Content-Type
很多人只关心Content-Type和Authorization。但我建议你再关注两个:
Accept—— 告诉服务器你期望的返回格式。比如application/json。If-Modified-Since/If-None-Match—— 用来做条件请求,减少传输量。
我记得有一次对接第三方API,对方文档说支持JSON和XML,但我怎么发都报406 Not Acceptable。后来发现我没传Accept头,服务器默认返回HTML。加了Accept: application/json之后一切正常。
RESTful架构设计原则
REST不是协议,是一套风格。它要求我们把网络上的操作看作是对“资源”的“表述”进行“转移”。听着玄乎,其实就三点:
- 用名词表示资源。比如
/api/users,而不是/api/getUsers。 - 用HTTP动词表示操作。GET查询,POST创建,PUT/PATCH更新,DELETE删除。
- 无状态。每次请求都是独立的,服务端不保存客户端上下文。
核心原则:URL只描述“是什么”,HTTP方法描述“做什么”。不要把动作写进URL里,比如/api/deleteUser?id=1就是坏味道。
为什么强调无状态?因为我曾经维护过一个状态码满飞的系统——服务端把用户信息放在Session里,结果多台机器一扩就乱套,用户登了A机器,请求却打到B机器,直接要求重新登录。改用JWT后,状态都放在里,任何节点都可以独立处理请求。解耦的第一步,就是把状态从服务端扔出去。
RESTful API 设计的最佳实践
URL路径设计
- 复数名词:
/api/orders而不是/api/order - 层级关系:
/api/users/{userId}/orders - 过滤、排序、分页用查询参数:
?status=paid&sort=-created_at&page=1&size=20
版本管理
我建议把版本号放在URL里:/v1/users。虽然有人主张用Accept: application/vnd.example.v1+json,但实际对接中还是URL更直观。新人一看就懂,不容易出错。
错误响应格式
统一返回错误码和消息,别让前端猜。举个例子:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户ID 9527 不存在",
"detail": "请检查用户ID是否正确"
}
}
这样前端拿到后可以直接展示给用户,或者按code做本地化。
注意:不要直接暴露数据库错误!比如Duplicate entry 'xxx' for key 'email_unique'这种内部信息,不能放到API响应里。用统一的业务错误码包一层,既安全又清晰。
一个完整示例(Flask)
我用Flask写过一个用户管理的RESTful接口,正反例都给你看看。先来反例:
# 坏味道 ❌
@app.route('/api/getUser')
def get_user_api():
uid = request.args.get('uid')
user = db.query(f"SELECT * FROM users WHERE id = {uid}")
return json.dumps(user)
问题是:动作拼在URL里、SQL注入、未处理异常。改成正例:
# 符合REST ✅
@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
user = User.query.get(user_id)
if not user:
return jsonify(error={
'code': 'USER_NOT_FOUND',
'message': f'用户 {user_id} 不存在'
}), 404
return jsonify(user.to_dict()), 200
看到了吗?URL只放资源标识,方法用GET,错误码返回404,消息描述清晰。
本节知识图解
下面这张图把HTTP请求和RESTful设计的关键点串起来了。你可以把它当作一张速查卡。
这张图从左到右展示了一次RESTful请求的完整路径:客户端带着方法和头,服务器处理完返回状态码。下半部分强调了设计原则。你可以把它截下来贴到日常笔记里。