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 大约束:

这 6 大约束是 RESTful 规范的"宪法",下面要讲的 10 条设计原则都是它们在接口设计层的具体落地。

二、RESTful API 10 条核心设计原则

原则 1:用名词不用动词 — URL 是"资源"不是"动作"

RESTful 的核心思想是"一切皆资源",URL 用来定位资源,而HTTP 动词用来描述对资源的操作。因此 URL 中不应该出现动词,所有动作都交给 HTTP Method 表达。

❌ 反例:GET /getUsersPOST /createUserGET /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    /articles           # 列表
GET    /articles/42        # 详情
POST   /articles           # 新建
PUT    /articles/42        # 整体替换
PATCH  /articles/42        # 部分字段更新
DELETE /articles/42        # 删除

原则 4:用 HTTP 状态码精准表达结果 — 不要"全是 200"

HTTP 状态码是客户端判断请求结果的第一信号。把业务错误塞进 200 是最常见的反模式,会让前端被迫解析 body 才能知道请求是否成功。请按下表使用:

把状态码用对,是 API 设计规范里最容易被忽视、却收益最大的一步。

原则 5:版本化 — 提前为演进留出空间

API 一定会变,但已发布的接口不应被悄悄破坏。最稳的工程实践是把版本号放进 URL 路径,例如 /api/v1/users,而不是埋在 Header 或 Query String 里——前者对人类最友好,对 CDN/网关最友好,对搜索引擎最友好。

三种主流版本化方式对比:

新版本应并行运行至少 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 种常见方案:

返回体建议带上分页元信息

{
  "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"
  }
}

要点:

原则 10:安全性 — HTTPS、CORS、限流、鉴权缺一不可

API 是系统的"门面",安全是底线。4 个必须做:

三、实战示例 — 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 中最高频出现的:

  1. URL 里塞动词/getUsers/createOrder,违反"URL 是资源不是动作"。
  2. 所有响应都返回 200:把业务错误塞进 { success: false, code: 1 },让前端必须解析 body。
  3. 不区分 POST/PUT/PATCH:建资源、整体更新、部分更新全用 POST,破坏幂等性。
  4. 不版本化/api/users 改字段直接上线,客户端一夜崩溃。
  5. 不分页GET /users 一次性返回 10 万行,OOM 是迟早的事。
  6. 错误信息五花八门:有时候 msg、有时候 message、有时候 error,前端写一堆 if-else。
  7. 忽略 HTTP 缓存:GET 接口不返回 ETag/Cache-Control,白白浪费 CDN 能力。
  8. 字段命名风格混用:一会 userId、一会 user_id、一会 userID,请选定 camelCase 或 snake_case 全局统一。
  9. 把所有错误都报 500:参数错误、权限不足、找不到资源统统 500,日志一片红,真正的问题被淹没。
  10. 明文 HTTP + 无鉴权:生产环境还在跑 http://api.xxx.com,登录接口无 HTTPS,token 直接裸奔。

把上面 10 条避掉,你的 API 在 95% 的场景下都是合格且专业的。

五、RESTful vs GraphQL — 怎么选?

讨论 REST 和 GraphQL 区别几乎是后端面试必问题。两者不是"谁取代谁"的关系,而是适用场景不同

一句话选型口诀:通用、稳态、高缓存 → 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 文档时顺手使用:

所有工具完全免费、无需注册、无广告干扰,欢迎收藏 devstoolbox.net 备用。