在 Node.js 后端开发中,API 是前后端交互的契约。一套清晰、一致的 RESTful 设计规范能显著降低沟通成本,让前端调用更直观,让后端代码更易维护。本节从实战角度梳理 RESTful API 的核心规范,涵盖 URL 设计、HTTP 方法、状态码、请求响应格式、分页、错误处理等关键方面。
17.2.1 URL 设计:资源导向,名词复数
REST 的核心思想是把服务端的业务数据抽象为资源,每个资源对应一个唯一的 URL。URL 只用来表述“资源在哪里”,而“对资源做什么操作”则由 HTTP 方法表达。
基本原则:
- 使用名词而非动词,一律用复数形式。
- 资源之间存在层级关系时,用嵌套 URL 表示,但嵌套层级不建议超过三层。
- 避免在 URL 中使用文件扩展名(如
.json、.xml)。
推荐示例:
| 动作 | 方法 | URL |
|------------------|--------|----------------------------------|
| 文章列表 | GET | /api/articles |
| 单篇文章 | GET | /api/articles/:id |
| 文章下的评论 | GET | /api/articles/:id/comments |
| 某条评论 | GET | /api/articles/:id/comments/:cid|
| 创建文章 | POST | /api/articles |
| 更新文章 | PUT | /api/articles/:id |
| 局部更新文章 | PATCH | /api/articles/:id |
| 删除文章 | DELETE | /api/articles/:id |
反例:
GET /api/getArticles // 动词,不规范
POST /api/createArticle // 动词 + 单数
GET /api/article // 名词单数
在 NestJS 或 Express 中,路由定义也遵循上述规范。例如 Express 中:
const router = express.Router();
router.get('/articles', articleController.list);
router.post('/articles', articleController.create);
router.get('/articles/:id', articleController.detail);
router.put('/articles/:id', articleController.update);
router.delete('/articles/:id', articleController.delete);
17.2.2 HTTP 方法的语义化使用
RESTful 规范要求 HTTP 方法必须符合其标准语义,不要用 GET 做删除操作,也不要全部用 POST 一把梭。
- GET:读取资源,安全且幂等,不应产生副作用。查询参数通过 query string 传递。
- POST:创建资源,非幂等(多次请求会创建多个资源)。请求体携带新建资源的数据。
- PUT:完整替换一个已有资源,幂等(同样的请求无论发送多少次,结果一致)。请求体包含完整的数据。
- PATCH:局部更新资源,通常只传递需要修改的字段,幂等性需自行保证。
- DELETE:删除资源,幂等。
日常实践中,POST 和 PUT 的区分常常模糊。如果前端不确定是完整替换还是局部更新,可以统一使用 POST 处理复杂更新逻辑,或者全部用 POST 避免歧义。但若追求标准化,应严格遵循上述语义,并在文档中清晰说明。
17.2.3 状态码:准确传达结果
HTTP 状态码是 API 的自解释能力。正确使用状态码能够避免前端写一堆 if (data.code === 0) 的判断,也更便于日志系统、监控系统识别异常。
常用状态码清单:
- 200 OK – 请求成功,适用于 GET、PUT、PATCH。
- 201 Created – 资源创建成功,通常用于 POST 请求,并在
Location头中返回新资源的 URL。 - 204 No Content – 操作成功但无需返回内容,常用于 DELETE 请求。
- 400 Bad Request – 客户端参数错误、格式错误、校验失败。
- 401 Unauthorized – 未认证,缺少或无效的 Token。
- 403 Forbidden – 已认证但无权限访问。
- 404 Not Found – 资源不存在(或故意隐藏)。
- 409 Conflict – 资源冲突,如重复创建。
- 422 Unprocessable Entity – 参数格式正确但语义有误(如缺少必填字段),是 400 的细化版本。
- 500 Internal Server Error – 服务器未知错误,不应包含敏感信息。
错误状态码的注意事项:
- 不要在 HTTP 200 的响应体中返回类似
{ "code": 500, "error": "xxx" }的错误信息——这会让客户端的错误处理机制失效,也混淆了监控指标。 - 始终让 HTTP 层面的状态码表达最终结果,业务逻辑错误也映射到对应的 4xx 状态码。
17.2.4 统一响应格式
为了便于前端解析和统一处理,每个接口应该遵循统一的 JSON 响应格式。推荐的结构如下:
成功响应:
{
"success": true,
"data": {
"id": 1,
"title": "Node.js 实战",
"author": "Ryan"
},
"message": "操作成功"
}
分页列表响应:
{
"success": true,
"data": {
"items": [...],
"total": 100,
"page": 1,
"pageSize": 20,
"totalPages": 5
}
}
错误响应:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "标题不能为空",
"details": [
{ "field": "title", "message": "标题是必填项" }
]
}
}
在实际编码中,通常会封装一个响应工具类,例如在 NestJS 中借助拦截器统一包装,或在 Express/Koa 中定义一个 res.success() 和 res.error() 的中间件。
17.2.5 请求与响应的数据格式
- 请求体和响应体统一使用 JSON 格式,设置
Content-Type: application/json。 - 日期时间字段推荐使用 ISO 8601 格式的字符串(如
"2024-10-14T08:30:00Z"),而不是时间戳,因为前者可读性好且保留时区。 - 布尔值、数字等保持原始类型,不要变成字符串。
- 避免过深的嵌套对象,单层、扁平的数据结构更容易被前端使用。
17.2.6 分页、排序与过滤
列表接口通常涉及大量数据,需要规范化分页、排序和过滤参数的设计。
分页:
使用 page 和 pageSize(或 limit)作为查询参数:
GET /api/articles?page=2&pageSize=20
排序:
使用 sortBy 和 order:
GET /api/articles?sortBy=createdAt&order=desc
过滤:
直接在查询参数中传递字段名,支持多条件:
GET /api/articles?status=published&authorId=10&keyword=Node
如果过滤条件复杂,可考虑使用类似 filter[status]=published&filter[category]=tech 的约定,但多数情况下简单字段平铺已经足够。
17.2.7 API 版本控制
API 一旦发布,后续修改需要保证不破坏现有客户端。版本控制是解决这一问题的标准手段。常见三种方式:
- URL 路径版本(最常用):
/api/v1/articles
/api/v2/articles
简单直观,便于路由分离,但理论上不够 RESTful(因为 URL 不应包含版本信息)。
- 请求头版本(较纯粹):
GET /api/articles
Accept: application/vnd.myapp.v1+json
保持了 URL 的纯粹性,但前端调试不便。
- 查询参数版本:
GET /api/articles?version=1
在 Node.js 项目中,URL 路径版本是主流做法,因为它与路由拆分天然吻合,也便于在网关层做请求分发。
17.2.8 错误处理与错误码设计
除了 HTTP 状态码,业务错误还需要内部错误码来帮助快速定位问题。错误码应全局唯一,采用大写字母 + 下划线风格,例如:
AUTH_EXPIRED_TOKEN– Token 过期RESOURCE_NOT_FOUND– 资源不存在PERMISSION_DENIED– 权限不足VALIDATION_ERROR– 参数校验失败
错误响应示例:
{
"success": false,
"error": {
"code": "ARTICLE_NOT_FOUND",
"message": "文章不存在或已被删除"
}
}
最佳实践:
- 线上环境不要返回堆栈、数据库错误等敏感信息,只在日志中记录。
- 全局错误处理中间件是统一错误封装的最佳位置,避免在每个路由中写重复的 try-catch。
17.2.9 安全与幂等性
- 所有数据写操作(POST/PUT/PATCH/DELETE)必须携带有效的认证信息(JWT、Session 等),在中间件层统一鉴权。
- 敏感接口需要防重放:对于涉及金额的接口,可以引入客户端生成的唯一
nonce+ 签名机制。 - 幂等性保证:PUT 和 DELETE 天然幂等,POST 的创建操作可以结合数据库唯一约束或业务层
idempotency key来避免重复创建。
17.2.10 文档与沟通
RESTful API 规范确立后,必须配合文档才能真正落地。推荐使用:
- Swagger/OpenAPI:在 NestJS 中直接使用
@nestjs/swagger,Express 中可用swagger-jsdoc。 - YApi、Apifox 等工具:方便前后端协作。
无论选择哪种方式,关键是把规范固化为代码中的注释或装饰器,自动生成可交互的文档。这样才能保证规范不流于纸面,成为团队真正的协作基线。
规范不是教条,不同的团队可以根据实际情况调整,但必须保持一致性。当你的 API 看起来像是由一个人设计的,而不是多个版本拼凑而成时,就说明规范真正发挥了作用。