🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)

🎬精选专栏传送门:
❄️《数据结构》 ❄️《AI与Agent那些事》
❄️《从0开始学计算机网络》 ❄️《后端开发》

前言:
一位前端从事者调接口,一会儿用 POST 拿数据,一会儿用 GET 删记录。URL 长这样:`/api/getUserInfo`、`/api/deleteUser?id=123`、`/api/userAction`。问后端这个接口是干嘛的,他说"你猜"。
结果就是前端天天问"这个接口返回啥格式",后端天天改接口不敢重构。联调两周,头发掉一半。
后来花了半天时间,把所有接口按照 RESTful 风格重新梳理了一遍。世界清静了。
这篇文章就聊聊 RESTful API 到底是怎么回事。
什么是 REST?
很多人一听 REST 就觉得是某种规范或者协议,其实不是。REST 是一种架构风格,更像是一种"约定俗成的默契"。
拿图书馆借书来类比。
假设图书馆有一套完整的图书管理系统。每一本书都是一个资源,比如《三体》这本书,它在系统里有一个唯一标识,可能是 `books/12345`。你想借书、还书、查书、预约,都是针对"书"这个资源做操作。
REST 的核心思想就一句话:把后端的数据和服务都看成资源,用 HTTP 方法表达对资源的操作。
资源:books/12345(《三体》这本书)
操作:查 -> GET books/12345
借 -> POST books/12345/borrow
还 -> PUT books/12345/return
你发现没有,URL 里全是名词,操作全在 HTTP 方法里。这就是 REST 和那种"URL 里写动词"(`/getBook`、`/deleteBook`)的根本区别。
REST 不是标准,没有 RFC 文档说"你必须这么做"。它是一种风格,大家约定俗成。好处是:只要大家都守规矩,接口的可读性和可维护性会好很多。
资源命名:URL 设计的黄金法则
资源命名是 RESTful 设计里最直观、也最容易做错的部分。几条黄金法则,记牢就行。
第一,用名词复数,不用动词。
✅ GET /users 获取用户列表
✅ GET /users/123 获取某个用户
✅ POST /users 创建用户
❌ GET /getUsers
❌ POST /createUser
第二,层级关系用斜杠表达。
✅ GET /users/123/orders 用户 123 的订单列表
✅ GET /users/123/orders/456 用户 123 的订单 456
❌ GET /getUserOrders?userId=123
第三,不用大写,用连字符。
URL 是区分大小写的,为了避免混乱,统一小写。多个单词用 `-` 连接,不要用下划线。
✅ GET /user-profiles
❌ GET /userProfiles
❌ GET /user_profiles
第四,不要暴露内部实现细节。
比如你的表叫 `t_user_info`,URL 别跟着叫 `/t_user_info`。API 是对外暴露的契约,应该用业务语言,不是技术语言。
这里有个小技巧:设计 URL 时,想象你是在浏览一个文件系统。`/users/123/orders/456` 就像路径 `users/123/orders/456`,一层一层往下走,清晰自然。
HTTP 方法怎么选:GET/POST/PUT/PATCH/DELETE
五个常用方法,每个都有明确语义。选对了,接口意图一目了然。
| 方法 | 用途 | 是否幂等 |
| GET | 查询资源 | ✅ 幂等 |
| POST | 创建资源 | ❌ 不幂等 |
| PUT | 整体更新资源 | ✅ 幂等 |
| PATCH | 局部更新资源 | ❌ 不幂等 |
| DELETE | 删除资源 | ✅ 幂等 |
GET 请求一百次,数据不会变。
PUT 把用户名字改成"张三",执行十次,结果还是"张三"。
DELETE 删除一个用户,删第一次成功了,再删就返回 404,但资源状态没变(还是不存在)。
但 POST 创建用户,执行十次就创建十个用户。PATCH 每次执行可能基于当前状态做修改,也可能不幂等。
这个特性在重试机制 里特别重要。比如网络超时,客户端不确定请求是否成功,就会重试。如果用的是 POST,重试可能导致数据重复创建。所以很多系统会引入**幂等键(Idempotency-Key)**来解决这个问题,但那是后话了。
看一个 Node.js + Express 的伪代码示例:
javascript
// 用户资源的路由设计
const express = require('express');
const router = express.Router();
// GET /users --- 获取用户列表
router.get('/users', (req, res) => {
// 从数据库查询用户列表
const users = db.findMany('users');
res.json({ data: users });
});
// GET /users/:id --- 获取单个用户
router.get('/users/:id', (req, res) => {
const user = db.findOne('users', req.params.id);
if (!user) {
return res.status(404).json({ error: '用户不存在' });
}
res.json({ data: user });
});
// POST /users --- 创建用户
router.post('/users', (req, res) => {
const newUser = db.insert('users', req.body);
// 201 = Created,带上新资源的完整信息
res.status(201).json({ data: newUser });
});
// PUT /users/:id --- 整体更新(客户端传完整对象)
router.put('/users/:id', (req, res) => {
const updated = db.replace('users', req.params.id, req.body);
res.json({ data: updated });
});
// PATCH /users/:id --- 局部更新(只传要改的字段)
router.patch('/users/:id', (req, res) => {
const patched = db.update('users', req.params.id, req.body);
res.json({ data: patched });
});
// DELETE /users/:id --- 删除用户
router.delete('/users/:id', (req, res) => {
db.remove('users', req.params.id);
// 204 = No Content,删除成功不返回 body
res.status(204).send();
});
注意几个细节:
POST 创建成功后返回 201,不是 200。
DELETE 成功后返回 204,不带 body。
PUT 和 PATCH 的区别:PUT 是"整体替换",客户端要传完整的资源对象;PATCH 是"局部修改",只传要改的字段。
状态码与错误处理:别只返回 200
很多新手写接口,不管成功失败都返回 200,然后在 body 里塞一个 `{ code: 500, message: "出错了" }`。
这个做法很坑。
HTTP 状态码本身就是协议的一部分,它有明确的语义。你返回 200,客户端就以为成功了,然后才发现 body 里有个 error,还得自己解析。这等于把 HTTP 协议废掉,自己发明了一套协议。
用正确的状态码,客户端可以直接根据状态码判断结果,省掉很多无谓的解析逻辑。
常用状态码速查:
| 状态码 | 含义 | 典型场景 |
| 200 | 成功 | GET/PUT/PATCH 成功 |
| 201 | 创建成功 | POST 创建资源 |
| 204 | 无内容 | DELETE 成功 |
| 400 | 请求参数错误 | 必填字段缺失、格式不对 |
| 401 | 未认证 | 没登录或 token 过期 |
| 403 | 无权限 | 登录了但没权限操作 |
| 404 | 资源不存在 | URL 写错或资源被删 |
| 409 | 冲突 | 创建重复资源、状态冲突 |
| 422 | 语义错误 | 请求格式对但业务上不合法 |
| 500 | 服务器内部错误 | 代码报错、数据库挂了 |
踩坑点:状态码用对了,但错误响应的格式五花八门。
有的接口返回 `{ error: "xxx" }`,有的返回 `{ message: "xxx" }`,有的返回 `{ msg: "xxx" }`。前端同事每次接新接口都要看文档才知道怎么取错误信息。
统一错误响应结构,约定一个格式:
javascript
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户不存在",
"details": "可选,补充说明"
}
}
或者更简单的:
javascript
{
"code": "USER_NOT_FOUND",
"message": "用户不存在"
}
关键是全局统一。不管哪个接口报错,格式都一样。前端可以写一个统一的错误拦截器,不用每个接口单独处理。
还有一点:业务错误码和 HTTP 状态码的关系。HTTP 状态码管"请求是否成功",业务错误码管"具体什么原因失败"。两者配合使用,不要混为一谈。
版本管理与过滤排序分页
接口上线后,需求总会变。用户字段要加、接口逻辑要改。但老版本的客户端还在用,不能直接改掉。所以版本管理很重要。
两种主流方式:
方式一:URL 路径版本号
javascript
GET /api/v1/users
GET /api/v2/users```
简单直观,容易路由,是目前最常用的方式。缺点是 URL 不够干净,但换来的是明确和好维护。
方式二:Header 版本号
javascript
GET /api/users
Accept: application/vnd.myapp.v2+json
URL 干净,但调试起来麻烦,而且对客户端要求高。适合对 API 纯净度有执念的团队。
对小白来说,无脑选 URL 路径版本号就行。等以后有需求了,再考虑 Header 方案。
过滤、排序、分页也是高频需求。REST 风格下,这些都用**查询参数**实现。
javascript
GET /api/v1/users?status=active&age=18&sort=-created_at&page=2&page_size=20
- `status=active` --- 过滤条件,多个条件用 `&` 连接
- `age=18` --- 精确过滤
- `sort=-created_at` --- 按创建时间倒序,`-` 表示倒序,没有 `-` 是正序
- `page=2&page_size=20` --- 第 2 页,每页 20 条
分页响应也要有约定:
javascript
{
"data": [...],
"pagination": {
"page": 2,
"page_size": 20,
"total": 156,
"total_pages": 8
}
}
把分页信息放在 `pagination` 字段里,前端处理起来就很方便。
踩坑点:
过滤参数不要搞得太花哨。比如 `filter=status:active,age:18` 这种 DSL 风格,看着高级,实际难维护。老老实实用 `?status=active&age=18` 就挺好。
总结:一张自查清单
RESTful API 设计的核心就四个字:面向资源 。一切围绕"资源"展开,URL 表达资源位置,HTTP 方法表达操作意图,状态码表达结果。
最后给一张自查清单,写完接口过一遍:
URL 用的是名词复数,没有动词?
层级关系用 `/` 表达,而不是查询参数?
没有大写字母,多词用 `-` 连接?
GET 只做查询,POST 只做创建?
PUT 整体更新,PATCH 局部更新?
创建返回 201,删除返回 204?
错误响应格式全局统一?
接口有版本号?
列表接口支持过滤、排序、分页?
这些规则不复杂,但真要做好,需要团队统一认知。建议把这份清单贴到团队文档里,下次评审接口设计时逐条过。
REST 不是终点。现在 GraphQL、gRPC 也都很流行,各有各的适用场景。但 REST 作为最基础、最通用的 API 设计风格,值得每个后端开发者掌握扎实。地基打牢了,学什么都快。