RESTful 接口实战:用户管理模块
🎯 引言
学完这篇文章,你将把本课程学过的路由、Controller、Service、参数校验、统一响应格式全部串起来,独立完成一个可增删改查的用户管理接口,并用前端代码调用它。这是一次完整的实战演练。
🛠 目标与文件清单
我们要实现一个用户管理模块,提供五个 RESTful 接口:
| 请求方法 | 路径 | 功能 |
|---|---|---|
| GET | /users | 获取用户列表 |
| GET | /users/:id | 获取单个用户 |
| POST | /users | 新增用户 |
| PUT | /users/:id | 修改用户 |
| DELETE | /users/:id | 删除用户 |
涉及的文件清单如下,每一篇学过的内容各占一个位置:
app
├── router.js # 路由:router.resources 一行注册
├── controller/user.js # 控制器:取参、校验、调用 Service、返回
├── service/user.js # 业务逻辑:内存数组的增删改查
└── extend/helper.js # 扩展:统一响应格式 success / fail
config
├── config.default.js # 配置:关闭 csrf 便于调试
└── plugin.js # 插件:启用 egg-validate
🛠 Service:数据操作层
数据用模块顶层的内存数组模拟,封装好五个方法:
const { Service } = require('egg');
// 用内存数组模拟数据库:模块顶层定义,所有请求共享,重启后还原
const users = [
{ id: 1, name: '小明', age: 18 },
{ id: 2, name: '小红', age: 20 },
];
let nextId = 3;
class UserService extends Service {
async findAll() {
return users;
}
async findById(id) {
return users.find(item => item.id === Number(id));
}
async create(data) {
const user = { id: nextId++, name: data.name, age: data.age };
users.push(user);
return user;
}
async update(id, data) {
const user = await this.findById(id);
if (!user) return null;
if (data.name !== undefined) user.name = data.name;
if (data.age !== undefined) user.age = data.age;
return user;
}
async remove(id) {
const index = users.findIndex(item => item.id === Number(id));
if (index === -1) return false;
users.splice(index, 1);
return true;
}
}
module.exports = UserService;
🛠 Controller:调度与校验
Controller 只做四件事:取参数、校验、调用 Service、返回统一格式。校验失败和资源不存在都有明确的处理:
const { Controller } = require('egg');
class UserController extends Controller {
// GET /users
async index() {
const { ctx } = this;
const users = await ctx.service.user.findAll();
ctx.helper.success(users);
}
// GET /users/:id
async show() {
const { ctx } = this;
const user = await ctx.service.user.findById(ctx.params.id);
if (!user) {
ctx.status = 404;
ctx.helper.fail('用户不存在');
return;
}
ctx.helper.success(user);
}
// POST /users
async create() {
const { ctx } = this;
try {
ctx.validate({
name: { type: 'string', required: true },
age: { type: 'number', required: true },
});
} catch (err) {
ctx.status = 422;
ctx.helper.fail('参数不完整或格式不正确');
return;
}
const user = await ctx.service.user.create(ctx.request.body);
ctx.status = 201;
ctx.helper.success(user);
}
// PUT /users/:id
async update() {
const { ctx } = this;
try {
ctx.validate({
name: { type: 'string', required: false },
age: { type: 'number', required: false },
});
} catch (err) {
ctx.status = 422;
ctx.helper.fail('参数格式不正确');
return;
}
const user = await ctx.service.user.update(ctx.params.id, ctx.request.body);
if (!user) {
ctx.status = 404;
ctx.helper.fail('用户不存在');
return;
}
ctx.helper.success(user);
}
// DELETE /users/:id
async destroy() {
const { ctx } = this;
const ok = await ctx.service.user.remove(ctx.params.id);
if (!ok) {
ctx.status = 404;
ctx.helper.fail('用户不存在');
return;
}
ctx.helper.success(null, '删除成功');
}
}
module.exports = UserController;
🛠 Router 与配置
路由一行搞定,再回顾一下配置文件:
module.exports = app => {
const { router, controller } = app;
router.get('/', controller.home.index);
router.resources('users', '/users', controller.user);
};
module.exports = {
validate: {
enable: true,
package: 'egg-validate',
},
};
module.exports = appInfo => {
const config = {};
config.keys = appInfo.name + '_my_secret_key';
// 学习阶段关闭 csrf 校验,方便调试接口,上线前必须开回来
config.security = {
csrf: {
enable: false,
},
};
return config;
};
helper 的 success / fail 沿用上一篇的封装,这里不再重复。
🛠 启动并测试
执行 npm run dev 启动服务,用 curl 依次测试五个接口:
# 获取列表
curl http://127.0.0.1:7001/users
# 新增
curl -X POST http://127.0.0.1:7001/users \
-H 'Content-Type: application/json' \
-d '{"name":"小刚","age":22}'
# 获取单个
curl http://127.0.0.1:7001/users/3
# 修改
curl -X PUT http://127.0.0.1:7001/users/3 \
-H 'Content-Type: application/json' \
-d '{"age":23}'
# 删除
curl -X DELETE http://127.0.0.1:7001/users/3
新增后再查列表,能看到数据已经共享生效;少传参数会收到 422 和统一的错误格式。也可以用 Apifox、Postman 这类接口工具测试,更直观。
🧱 前端调用示例
接口写好,前端用 fetch 就能对接。以获取列表和新增为例:
const baseURL = 'http://127.0.0.1:7001';
// 获取用户列表
const res = await fetch(baseURL + '/users');
const result = await res.json();
if (result.code === 0) {
console.log('用户列表:', result.data);
} else {
console.log('请求失败:', result.message);
}
// 新增用户
const addRes = await fetch(baseURL + '/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '小丽', age: 21 }),
});
const addResult = await addRes.json();
console.log(addResult.message);
统一响应格式的好处在这里体现出来了:前端只要判断 code 就能处理所有接口,不用为每个接口写不同的解析逻辑。
egg-cors 插件解决,学习阶段可以直接在 HTML 文件里跑上面的代码验证。⚠️ 内存数据的局限
这套代码用于学习足够,但内存数组有两个明显的局限:
- 重启丢失:服务一重启,新增的数据就没了。
- 多实例不共享:生产环境多个进程各存一份,数据会不一致。
真实项目的下一步是把 Service 里的数组操作换成数据库操作。因为业务都收敛在 Service 层,届时只需要改这一个文件,Controller 完全不用动,这正是分层架构的价值。
🧾 小节总结
- 一个完整的 RESTful 模块 =
router.resources路由 + Controller 调度 + Service 数据操作 + helper 统一响应 + egg-validate 参数校验。 - Controller 保持四步:取参、校验、调用 Service、返回;业务细节全部下沉到 Service。
- 校验失败返回 422,资源不存在返回 404,新增成功返回 201,响应体始终走统一格式。
- 前端通过判断
code统一处理响应;跨域问题可用egg-cors插件解决。 - 内存数组只是学习方案,接数据库时只需修改 Service 层。
❓ 知识问答
Q1:为什么 update 的校验规则里 required 是 false?
修改接口允许只传部分字段(比如只改年龄),所以字段都设为可选。Service 里也只更新传了的字段,这就是常说的“部分更新”。
Q2:删除成功为什么要返回 200 而不是别的状态码?
RESTful 实践中删除成功返回 200 或 204 都常见。我们为了响应体格式统一(前端能拿到 message),选择了 200 加统一结构,团队内约定一致即可。
Q3:想给接口加上分页,改动大吗?
不大。在 findAll 里接收 page、pageSize 参数做数组切片,Controller 从 ctx.query 取参传进去,再配合上一篇练习里的 helper.page 返回即可。
Q4:这套结构能直接用到公司项目里吗?
分层结构和约定是可以直接借鉴的。但真实项目还需要数据库、登录鉴权、日志、参数更严格的校验等,这些是进阶内容,可以在此基础上逐步补齐。
🧪 小练习
给接口加分页功能:GET /users?page=1&pageSize=10,Service 中对数组切片,返回时用统一格式带上总数。
// 在 UserService 中新增方法
async findByPage(page, pageSize) {
// 请在这里编写代码
}
// 改造 index 方法
async index() {
const { ctx } = this;
// 请在这里编写代码
}
🎉 恭喜你完成了用户管理模块的实战,也走完了 Egg.js 课程的全部内容!从脚手架创建项目到分层架构、插件扩展,你已经具备了用 Egg.js 开发接口的完整能力。接下来可以尝试接入数据库(如 egg-mysql、egg-sequelize),把这套结构用在真实项目中。
