Node.js RESTful API 设计
接口的 URL 和状态码怎么设计,直接决定前后端联调的效率。上一篇 Node.js Express 中间件 把框架机制讲完,这篇讲接口本身的设计规范。RESTful 是目前最主流的接口风格,核心思想是资源导向,URL 表达资源,HTTP 方法表达操作。
资源导向
把一切数据都看成资源,URL 用名词复数表示资源,HTTP 方法表示对资源做什么。
| 错误写法 | 正确写法 | 说明 |
|---|---|---|
| /getUsers | GET /users | 查询用方法表达 |
| /deleteUser/7 | DELETE /users/7 | 删除用方法表达 |
| /createUser | POST /users | 创建用方法表达 |
动词进 URL 是典型反模式,/getUser、/deleteUser 这类接口会让 URL 列表越写越乱,前端也无法用统一规则推断接口。资源名永远用名词,操作交给方法。
URL 设计
| 写法 | 含义 |
|---|---|
| /users | 用户资源集合 |
| /users/:id | 单个用户资源 |
| /users/:id/posts | 嵌套资源,某用户的文章 |
| /users?page=2&size=10 | 查询参数表达筛选分页 |
嵌套资源表达从属关系,比如 /users/7/posts 表示用户 7 的文章列表。多层嵌套会难读难维护,一般嵌套一层就够,再深就拆开。单个资源的地址应该稳定,前端把它当作数据的唯一地址使用。
HTTP 方法和操作
| 方法 | 操作 | 幂等 |
|---|---|---|
| GET | 查询资源 | 是 |
| POST | 创建资源 | 否 |
| PUT | 整体更新资源 | 是 |
| DELETE | 删除资源 | 是 |
幂等指同一个请求重复执行多次,结果和第一次相同。GET、PUT、DELETE 都是幂等的,重复请求不会产生额外副作用。POST 不幂等,每发一次就新建一条数据。PUT 是整体更新,用完整数据替换目标资源,只改一个字段的局部更新用 PATCH。
状态码
HTTP 状态码是语义的一部分,选错会让客户端处理逻辑出错。
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | 成功 | 查询、更新、删除成功 |
| 201 | 已创建 | POST 创建成功 |
| 204 | 无内容 | 成功但不需要返回数据 |
| 400 | 参数错误 | 缺字段、格式不对 |
| 401 | 未认证 | 没登录或 token 失效 |
| 403 | 无权限 | 已登录但没有权限 |
| 404 | 资源不存在 | 路径或资源找不到 |
| 500 | 服务器错误 | 代码异常 |
状态码只用标准语义,别自己发明 2xx 表达错误。参数问题给 400,登录相关给 401,权限不足给 403,三者场景完全不同。
统一响应格式
接口返回的数据结构要统一,前端才能写一套解析逻辑。常用结构是 { code, data, message } 三件套。
// response.js,封装统一的响应函数
function success(res, data, message = "ok") {
res.json({ code: 0, data, message });
}
function fail(res, status, message) {
res.status(status).json({ code: status, data: null, message });
}
module.exports = { success, fail };
code 是业务码,0 表示成功,非 0 表示失败。message 给前端直接展示用。HTTP 状态码描述传输层面的结果,业务码描述业务层面的结果,两者分开,前端先看 HTTP 状态码,再看 code。
// 使用方式
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) {
return fail(res, 404, "用户不存在");
}
success(res, user);
});
常见坑
状态码用错
删除一个不存在的资源返回 500,这是服务器错误码,实际是客户端传了无效 ID,该给 404。拿不准时按这个顺序判断,先看客户端有没有错(400 系列),再看资源在不在(404),最后才是服务器问题(500)。
用 POST 做更新
POST 语义是创建,重复提交会重复创建数据。更新必须用 PUT,删除必须用 DELETE,方法语义和操作保持一致,别混用。
响应格式不统一
有的接口返回 { code, data },有的直接 res.send 字符串,前端每种情况都要单独处理。从第一个接口开始就用统一的 success 和 fail,后面维护会省很多事。
实践
用内存数组实现用户管理的完整 CRUD 接口,统一响应格式一次到位。数据先放内存,重启就丢,数据库篇会把它换成真数据库。
// server.js
const express = require("express");
const { success, fail } = require("./response");
const app = express();
app.use(express.json());
// 内存数据库,重启后数据丢失
let users = [
{ id: 1, name: "张三", age: 20 },
{ id: 2, name: "李四", age: 18 },
];
let nextId = 3;
// 查询用户列表,支持 ?age=18 筛选
app.get("/users", (req, res) => {
const { age } = req.query;
const result = age ? users.filter((u) => u.age === Number(age)) : users;
success(res, result);
});
// 查询单个用户
app.get("/users/:id", (req, res) => {
const user = users.find((u) => u.id === Number(req.params.id));
if (!user) {
return fail(res, 404, "用户不存在");
}
success(res, user);
});
// 创建用户
app.post("/users", (req, res) => {
const { name, age } = req.body;
if (!name || !age) {
return fail(res, 400, "name 和 age 必填");
}
const user = { id: nextId++, name, age };
users.push(user);
success(res, user, "创建成功");
});
// 整体更新用户
app.put("/users/:id", (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return fail(res, 404, "用户不存在");
}
users[index] = { id, ...req.body };
success(res, users[index], "更新成功");
});
// 删除用户
app.delete("/users/:id", (req, res) => {
const id = Number(req.params.id);
const index = users.findIndex((u) => u.id === id);
if (index === -1) {
return fail(res, 404, "用户不存在");
}
users.splice(index, 1);
success(res, null, "删除成功");
});
app.listen(3000, () => {
console.log("服务器已启动");
});
node server.js
# 逐个测试,注意观察状态码和响应结构
curl http://localhost:3000/users
curl http://localhost:3000/users/1
curl -X POST -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":25}" http://localhost:3000/users
curl -X PUT -H "Content-Type: application/json" -d "{\"name\":\"王五\",\"age\":26}" http://localhost:3000/users/3
curl -X DELETE http://localhost:3000/users/2
# 错误场景
curl http://localhost:3000/users/99
curl -X POST -H "Content-Type: application/json" -d "{}" http://localhost:3000/users
接口规范到这里统一了,端口号还是写死的 3000,下一篇 Node.js 环境变量与配置 解决不同环境配置不同的问题。