理解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不是协议,是一套风格。它要求我们把网络上的操作看作是对“资源”的“表述”进行“转移”。听着玄乎,其实就三点:

  1. 用名词表示资源。比如/api/users,而不是/api/getUsers。
  2. 用HTTP动词表示操作。GET查询,POST创建,PUT/PATCH更新,DELETE删除。
  3. 无状态。每次请求都是独立的,服务端不保存客户端上下文。

核心原则: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设计的关键点串起来了。你可以把它当作一张速查卡。

HTTP请求与RESTful设计要点 客户端 (Client) HTTP方法 GET / POST / PUT PATCH / DELETE 请求头 Content-Type Authorization / Accept 服务器 (Server) 状态码 2xx 成功 (200, 201) 4xx 客户端错误 (400,401,403,404) 5xx 服务端错误 (500,503) RESTful 核心原则 1. 用名词描述资源(/users/{id}) 2. HTTP动词表示操作(GET查,POST增,PUT改,DELETE删) 3. 无状态(不依赖服务端Session)

这张图从左到右展示了一次RESTful请求的完整路径:客户端带着方法和头,服务器处理完返回状态码。下半部分强调了设计原则。你可以把它截下来贴到日常笔记里。