RESTful API 设计规范完全指南 — 10 条核心原则 + Node.js 实战示例
在前后端分离、微服务架构、移动端与 Web 端并行的今天,RESTful API 设计已经成为每一位后端工程师、前端工程师乃至全栈开发者的必备技能。一套优雅、规范、可维护的 API 接口不仅能显著提升团队协作效率,还能让你的服务在多年演进之后依然保持清晰的边界与良好的可读性。无论是初创团队的 MVP,还是日均调用数十亿次的大型平台,遵循统一的 RESTful 规范都是降低沟通成本、减少 Bug、提升可观测性的关键。
然而在实际工作中,我们见过太多"能用但难看"的 API:URL 里塞满动词、状态码永远 200、错误信息五花八门、版本号一改就要重构客户端。这些反模式不仅拖慢迭代速度,更会让新成员在 onboarding 时一头雾水。本篇 RESTful 教程将系统性地拆解 API 设计原则与 API 设计规范,覆盖 REST 架构的 6 大约束、10 条最核心的设计原则、一套可落地的 Node.js/Express 实战示例、10 个最常见的反模式,并深度对比 REST 和 GraphQL 区别,帮助你一次性建立完整的 RESTful 知识体系。
一、什么是 REST 架构 — 6 大核心约束
REST(Representational State Transfer,表述性状态转移)由 Roy Fielding 在 2000 年的博士论文中提出,它不是一种协议,也不是一种标准,而是一套面向资源的架构风格。判断一个系统是否"RESTful"的关键,是看它是否满足以下 6 大约束:
- 客户端-服务器分离(Client-Server):UI 与数据存储关注点分离,提升跨平台可移植性与服务端可伸缩性。
- 无状态(Stateless):每个请求都必须包含服务器理解它所需的全部信息,服务器不保存客户端会话状态。
- 可缓存(Cacheable):响应必须明确标记是否可缓存,从而减少客户端-服务器交互、提升性能。
- 统一接口(Uniform Interface):通过资源标识、表述操作、自描述消息、超媒体即应用状态(HATEOAS)来解耦架构。
- 分层系统(Layered System):客户端无法判断自己是否直接连到终端服务器,可借助中间层(代理、网关、CDN)提升伸缩性。
- 按需代码(Code-On-Demand,可选):服务器可通过传输可执行代码(如 JavaScript)临时扩展客户端功能。
这 6 大约束是 RESTful 规范的"宪法",下面要讲的 10 条设计原则都是它们在接口设计层的具体落地。
二、RESTful API 10 条核心设计原则
原则 1:用名词不用动词 — URL 是"资源"不是"动作"
RESTful 的核心思想是"一切皆资源",URL 用来定位资源,而HTTP 动词用来描述对资源的操作。因此 URL 中不应该出现动词,所有动作都交给 HTTP Method 表达。
❌ 反例:GET /getUsers、POST /createUser、GET /deleteUser?id=1
✅ 正例:
GET /users # 获取用户列表
GET /users/123 # 获取 id=123 的用户
POST /users # 创建用户
PUT /users/123 # 更新 id=123 的用户
DELETE /users/123 # 删除 id=123 的用户把动词从 URL 移到 Method 后,你的 API 就具备了"自描述性"——只看 URL + Method 就能理解意图,而不用读文档。
原则 2:用复数资源名 — 保持集合与单体的一致性
资源集合应当使用复数名词,让"列表"和"单个元素"在 URL 风格上保持一致。即使某个资源在业务上永远只有一条(如 /settings),也建议用复数,以避免未来扩展时被迫改名。
❌ GET /user/123 → ✅ GET /users/123
❌ GET /article/5 → ✅ GET /articles/5
复数命名也方便后续做批量操作(POST /users/batch-delete)与聚合资源(GET /users/active)。
原则 3:用 HTTP 动词表意 — 5 个 Method 覆盖 CRUD
RESTful API 设计原则要求严格使用 HTTP 动词表达语义,这是统一接口的核心。常用 5 个动词的语义如下:
- GET:幂等、安全(不会改变服务端状态),用于读取资源。
- POST:非幂等,用于创建资源或触发非幂等动作(如登录、下单)。
- PUT:幂等,用于完整替换资源(客户端必须传完整字段)。
- PATCH:可用于部分更新(传什么改什么),适合"增量更新"场景。
- DELETE:幂等,用于删除资源。
示例:
GET /articles # 列表
GET /articles/42 # 详情
POST /articles # 新建
PUT /articles/42 # 整体替换
PATCH /articles/42 # 部分字段更新
DELETE /articles/42 # 删除原则 4:用 HTTP 状态码精准表达结果 — 不要"全是 200"
HTTP 状态码是客户端判断请求结果的第一信号。把业务错误塞进 200 是最常见的反模式,会让前端被迫解析 body 才能知道请求是否成功。请按下表使用:
- 2xx 成功:200 OK(一般成功)、201 Created(新建成功)、204 No Content(删除/无 body 成功)。
- 3xx 重定向:304 Not Modified(缓存命中)、301/302(资源位置变更)。
- 4xx 客户端错误:400 Bad Request(参数错误)、401 Unauthorized(未认证)、403 Forbidden(已认证但无权限)、404 Not Found、409 Conflict(资源冲突)、422 Unprocessable Entity(参数语义错误)、429 Too Many Requests(限流)。
- 5xx 服务端错误:500 Internal Server Error、502 Bad Gateway、503 Service Unavailable、504 Gateway Timeout。
把状态码用对,是 API 设计规范里最容易被忽视、却收益最大的一步。
原则 5:版本化 — 提前为演进留出空间
API 一定会变,但已发布的接口不应被悄悄破坏。最稳的工程实践是把版本号放进 URL 路径,例如 /api/v1/users,而不是埋在 Header 或 Query String 里——前者对人类最友好,对 CDN/网关最友好,对搜索引擎最友好。
三种主流版本化方式对比:
- URI 路径(推荐):
/api/v1/users,直观、便于路由。 - Query 参数:
/api/users?version=1,不破坏 RESTful 风格但难做网关转发。 - Header 媒体类型:
Accept: application/vnd.myapi.v1+json,最 RESTful 但对前端不友好。
新版本应并行运行至少 6~12 个月,给客户端充分的迁移窗口。
原则 6:用 JSON 不用 XML — 但保留 Content-Type 协商
在 2026 年的今天,JSON 已经是事实标准:体积小、可读性强、与 JavaScript 原生兼容。除非你的客户强制要求 XML(如某些金融/政企系统),否则一律 Content-Type: application/json; charset=utf-8。
返回示例:
{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
"created_at": "2026-06-08T10:00:00Z"
}同时请在响应中显式声明 Content-Type,不要让客户端"猜"。
原则 7:过滤、排序、分页 — 集合接口的标配
一旦资源集合可能很大,就必须支持过滤(filter)、排序(sort)、分页(pagination)。推荐使用 query string:
GET /api/v1/orders?status=active&sort=-created_at&page=1&limit=20
GET /api/v1/orders?created_after=2026-01-01&search=phone
GET /api/v1/orders?fields=id,total,status # 字段过滤(sparse fieldsets)分页有 3 种常见方案:
- Offset 分页:
?page=1&limit=20,实现简单但深翻页性能差。 - Cursor 分页:
?cursor=eyJpZCI6MTIzfQ==&limit=20,性能稳定,适合无限滚动。 - Keyset 分页:
?after_id=123&limit=20,结合排序字段,大数据集首选。
返回体建议带上分页元信息:
{
"data": [ ... ],
"meta": { "page": 1, "limit": 20, "total": 1024, "has_more": true }
}原则 8:嵌套资源用路径表达"从属关系"
当一个资源天然属于另一个资源时,用路径嵌套表达从属关系:
GET /users/123/orders # 用户 123 的所有订单
GET /users/123/orders/456 # 用户 123 的订单 456
POST /users/123/orders # 给用户 123 创建订单
GET /articles/42/comments # 文章 42 的评论但请注意:嵌套层级不要超过 3 层,否则 URL 会变得难以维护。超过 3 层时改用顶级资源 + 过滤:
GET /orders?user_id=123 # 优于 /users/123/orders/recent/top-5原则 9:错误信息统一格式 — 让客户端能"机读"
好的错误响应应该既人可读、又机可读。推荐使用统一结构:
{
"error": {
"code": "USER_NOT_FOUND",
"message": "用户 id=999 不存在",
"details": {
"resource": "user",
"id": 999
},
"request_id": "req_8f3a2b"
}
}要点:
- code:机器可读的字符串枚举,前端可直接
switch。 - message:人类可读的本地方言字符串(i18n 时按
Accept-Language返回)。 - details:可放字段级错误(validation)、链接文档等。
- request_id:服务端日志追踪 ID,问题排查时一键定位。
原则 10:安全性 — HTTPS、CORS、限流、鉴权缺一不可
API 是系统的"门面",安全是底线。4 个必须做:
- HTTPS(TLS 1.2+):所有 API 必须走 HTTPS,禁用 HTTP 明文传输。Let's Encrypt 已是免费事实标准。
- CORS:跨域请求必须显式配置
Access-Control-Allow-Origin,不要无脑*,对带凭证的请求尤其要精确到域名。 - 限流(Rate Limiting):用令牌桶或滑动窗口算法限制单 IP/单用户的 QPS,返回
429+Retry-AfterHeader。推荐用 Redis + Lua 脚本实现。 - 鉴权(Authentication & Authorization):
- 无状态首选 JWT(OAuth 2.0 / OIDC)。
- 服务端渲染可保留 Session-Cookie。
- 高危操作加 二次验证(2FA / OTP)。
- 所有写接口强制 CSRF 防护。
三、实战示例 — Node.js/Express 实现 Todo API
理论讲完,上代码。下面是一套生产级最小可用的 Todo API,涵盖 GET/POST/PUT/PATCH/DELETE 5 个端点 + JWT 鉴权 + 统一错误格式 + 限流,复制即可运行:
// app.js
import express from 'express';
import jwt from 'jsonwebtoken';
import rateLimit from 'express-rate-limit';
const app = express();
app.use(express.json());
app.use(rateLimit({ windowMs: 60_000, max: 100 })); // 100 req/min/IP
const SECRET = process.env.JWT_SECRET || 'dev-secret-change-me';
let todos = [{ id: 1, title: 'Learn REST', done: false }];
let nextId = 2;
// ----- 统一错误中间件 -----
const errorHandler = (err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
error: {
code: err.code || 'INTERNAL_ERROR',
message: err.message || 'Server Error',
request_id: req.id
}
});
};
// ----- JWT 鉴权中间件 -----
const auth = (req, res, next) => {
const token = (req.headers.authorization || '').replace('Bearer ', '');
try { req.user = jwt.verify(token, SECRET); next(); }
catch { next({ status: 401, code: 'UNAUTHORIZED', message: '无效或缺失 Token' }); }
};
// ----- 5 个 RESTful 端点 -----
app.get ('/api/v1/todos', auth, (req, res) => res.json({ data: todos }));
app.get ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo 不存在' });
res.json({ data: t });
});
app.post ('/api/v1/todos', auth, (req, res) => {
const t = { id: nextId++, title: req.body.title, done: false };
todos.push(t);
res.status(201).json({ data: t });
});
app.put ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo 不存在' });
Object.assign(t, req.body); // 整体替换
res.json({ data: t });
});
app.patch ('/api/v1/todos/:id', auth, (req, res) => {
const t = todos.find(x => x.id === +req.params.id);
if (!t) return next({ status: 404, code: 'TODO_NOT_FOUND', message: 'Todo 不存在' });
Object.assign(t, req.body); // 部分更新
res.json({ data: t });
});
app.delete('/api/v1/todos/:id', auth, (req, res) => {
todos = todos.filter(x => x.id !== +req.params.id);
res.status(204).end();
});
app.use(errorHandler);
app.listen(3000);测试请求(用 curl):
# 登录拿 Token
curl -X POST http://localhost:3000/login -d '{"user":"alice","pass":"x"}' -H 'Content-Type: application/json'
# 创建 Todo
curl -X POST http://localhost:3000/api/v1/todos -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' -d '{"title":"写完 RESTful 教程"}'
# 部分更新(PATCH)
curl -X PATCH http://localhost:3000/api/v1/todos/1 -H "Authorization: Bearer <token>" -H 'Content-Type: application/json' -d '{"done":true}'约 60 行代码,就完整覆盖了RESTful API 设计的 10 条核心原则。
四、10 个最常见的 RESTful 反模式
看完正例,再来排雷。下面这 10 个错误,是 Code Review 中最高频出现的:
- URL 里塞动词:
/getUsers、/createOrder,违反"URL 是资源不是动作"。 - 所有响应都返回 200:把业务错误塞进
{ success: false, code: 1 },让前端必须解析 body。 - 不区分 POST/PUT/PATCH:建资源、整体更新、部分更新全用 POST,破坏幂等性。
- 不版本化:
/api/users改字段直接上线,客户端一夜崩溃。 - 不分页:
GET /users一次性返回 10 万行,OOM 是迟早的事。 - 错误信息五花八门:有时候
msg、有时候message、有时候error,前端写一堆 if-else。 - 忽略 HTTP 缓存:GET 接口不返回
ETag/Cache-Control,白白浪费 CDN 能力。 - 字段命名风格混用:一会
userId、一会user_id、一会userID,请选定 camelCase 或 snake_case 全局统一。 - 把所有错误都报 500:参数错误、权限不足、找不到资源统统 500,日志一片红,真正的问题被淹没。
- 明文 HTTP + 无鉴权:生产环境还在跑
http://api.xxx.com,登录接口无 HTTPS,token 直接裸奔。
把上面 10 条避掉,你的 API 在 95% 的场景下都是合格且专业的。
五、RESTful vs GraphQL — 怎么选?
讨论 REST 和 GraphQL 区别几乎是后端面试必问题。两者不是"谁取代谁"的关系,而是适用场景不同:
- REST 优势:简单、HTTP 原生、缓存友好、学习成本低、工具链成熟(Swagger/OpenAPI、Postman、curl 一把梭)。适合面向公众的开放 API、CRUD 为主、需要 CDN 缓存的场景。
- GraphQL 优势:单一端点、客户端按需取字段、无需多版本、强类型 schema。适合前端多变、聚合多数据源、移动端弱网场景。
一句话选型口诀:通用、稳态、高缓存 → REST;多端、聚合、迭代快 → GraphQL。中小团队默认选 REST,复杂场景再考虑 GraphQL 或 BFF(Backend-For-Frontend)模式。
六、常见问题 FAQ
Q1:REST 和 GraphQL 怎么选?
看团队规模与业务复杂度。REST 简单稳定、缓存友好,适合开放平台和 CRUD 业务;GraphQL 灵活聚合、一次请求拿全数据,适合多端复杂场景。如果你是初创团队或在做 MVP,优先 REST,GraphQL 带来的工程复杂度(schema registry、查询复杂度限制、缓存)往往得不偿失。
Q2:GET 请求能传 body 吗?
技术上可以,实践上不要。HTTP/1.1 规范并未禁止 GET 带 body,但绝大多数代理、CDN、客户端、调试工具会丢弃 GET 的 body。请用 query string(?a=1&b=2)或改用 POST(语义是"非幂等的查询",比如复杂搜索)。
Q3:PUT 和 PATCH 区别?
PUT = 整体替换,客户端必须传完整资源;缺字段会被清空。PATCH = 局部更新,只传要改的字段,未传字段保持不变。例:用户资料修改,"只改昵称"用 PATCH,"覆盖整个用户卡"用 PUT。
Q4:HTTP 状态码 401 和 403 区别?
401 Unauthorized = "你还没证明你是谁",通常用于未登录或 Token 失效。403 Forbidden = "你证明了身份,但没有权限",用于已登录但越权操作。简单记:401 是"请先登录",403 是"别碰这个"。
Q5:怎么设计安全的 API?
5 条铁律:(1) 全站 HTTPS;(2) JWT/OAuth2 鉴权,Token 短期 + Refresh 长期;(3) 所有写接口 CSRF 防护 + 来源校验;(4) 全量限流(IP + User 双维度)+ 关键接口图形/短信验证码;(5) 完整审计日志,关键操作可追溯。
七、写在最后 — 把工具用起来
掌握 RESTful API 设计规范,是为了写出更可维护的服务;而把重复劳动交给工具,则是为了把时间留给真正有创造力的工作。DevToolbox(devstoolbox.net)目前已上线 100+ 免费在线开发者工具,覆盖 PDF、JSON、加密、编码、正则、时间戳、UUID 等常见场景,纯前端运行、数据零上传,特别适合在写 API 文档、调试接口、整理 PDF 文档时顺手使用:
- PDF 合并:把多份 API 文档、Swagger 导出、Postman 报告一键合并成一个 PDF。
- PDF 压缩:API 文档太大发不出去?在线无损/有损压缩,浏览器里即可完成。
- PDF 转 Word:把产品 PRD 转成 Word 继续编辑,支持表格与图片保留。
- JSON 格式化:粘贴 API 响应一键美化、压缩、校验,配合路径分析定位字段。
- JWT 解码:把请求头里的 Bearer Token 粘进来,立刻看到 Header / Payload / Signature。
- UUID 生成器:调试幂等性、写测试用例时快速生成 v1/v4/v7 UUID。
- Base64 编解码:调试 Basic Auth、解析请求头里的二进制 Payload 时必备。
- URL 编码:处理 query string 中的中文、特殊字符、转义问题。
- HTTP 状态码速查:400 vs 422、401 vs 403 还在记混?一键查询。
- 正则测试器:写路由参数、参数校验时实时调试正则。
所有工具完全免费、无需注册、无广告干扰,欢迎收藏 devstoolbox.net 备用。