【从0开始学习计算机网络】| RESTful API 设计原则

【从0开始学习计算机网络】| RESTful API 设计原则
个人主页:一条泥憨鱼(欢迎各位大佬莅临)精选专栏传送门:❄️《数据结构》 ❄️《AI与Agent那些事》❄️《从0开始学计算机网络》 ❄️《后端开发》前言一位前端从事者调接口一会儿用 POST 拿数据一会儿用 GET 删记录。URL 长这样/api/getUserInfo、/api/deleteUser?id123、/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?userId123第三不用大写用连字符。URL 是区分大小写的为了避免混乱统一小写。多个单词用 - 连接不要用下划线。✅ GET /user-profiles❌ GET /userProfiles❌ GET /user_profiles第四不要暴露内部实现细节。比如你的表叫 t_user_infoURL 别跟着叫 /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 的伪代码示例// 用户资源的路由设计 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 }。前端同事每次接新接口都要看文档才知道怎么取错误信息。统一错误响应结构约定一个格式{ error: { code: USER_NOT_FOUND, message: 用户不存在, details: 可选补充说明 } }或者更简单的{ code: USER_NOT_FOUND, message: 用户不存在 }关键是全局统一。不管哪个接口报错格式都一样。前端可以写一个统一的错误拦截器不用每个接口单独处理。还有一点业务错误码和 HTTP 状态码的关系。HTTP 状态码管请求是否成功业务错误码管具体什么原因失败。两者配合使用不要混为一谈。版本管理与过滤排序分页接口上线后需求总会变。用户字段要加、接口逻辑要改。但老版本的客户端还在用不能直接改掉。所以版本管理很重要。两种主流方式方式一URL 路径版本号GET /api/v1/users GET /api/v2/users简单直观容易路由是目前最常用的方式。缺点是 URL 不够干净但换来的是明确和好维护。方式二Header 版本号GET /api/users Accept: application/vnd.myapp.v2jsonURL 干净但调试起来麻烦而且对客户端要求高。适合对 API 纯净度有执念的团队。对小白来说无脑选 URL 路径版本号就行。等以后有需求了再考虑 Header 方案。过滤、排序、分页也是高频需求。REST 风格下这些都用**查询参数**实现。GET /api/v1/users?statusactiveage18sort-created_atpage2page_size20 - statusactive — 过滤条件多个条件用 连接 - age18 — 精确过滤 - sort-created_at — 按创建时间倒序- 表示倒序没有 - 是正序 - page2page_size20 — 第 2 页每页 20 条分页响应也要有约定{ data: [...], pagination: { page: 2, page_size: 20, total: 156, total_pages: 8 } }把分页信息放在 pagination 字段里前端处理起来就很方便。踩坑点过滤参数不要搞得太花哨。比如 filterstatus:active,age:18 这种 DSL 风格看着高级实际难维护。老老实实用 ?statusactiveage18 就挺好。总结一张自查清单RESTful API 设计的核心就四个字面向资源。一切围绕资源展开URL 表达资源位置HTTP 方法表达操作意图状态码表达结果。最后给一张自查清单写完接口过一遍- [ ] URL 用的是名词复数没有动词- [ ] 层级关系用 / 表达而不是查询参数- [ ] 没有大写字母多词用 - 连接- [ ] GET 只做查询POST 只做创建- [ ] PUT 整体更新PATCH 局部更新- [ ] 创建返回 201删除返回 204- [ ] 错误响应格式全局统一- [ ] 接口有版本号- [ ] 列表接口支持过滤、排序、分页这些规则不复杂但真要做好需要团队统一认知。建议把这份清单贴到团队文档里下次评审接口设计时逐条过。REST 不是终点。现在 GraphQL、gRPC 也都很流行各有各的适用场景。但 REST 作为最基础、最通用的 API 设计风格值得每个后端开发者掌握扎实。地基打牢了学什么都快。