Egg.js 简介

RESTful 接口实战:用户管理模块

综合运用路由、Controller、Service、参数校验与统一响应格式,完成一个完整的用户管理 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:数据操作层

数据用模块顶层的内存数组模拟,封装好五个方法:

app/service/user.js
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、返回统一格式。校验失败和资源不存在都有明确的处理:

app/controller/user.js
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 与配置

路由一行搞定,再回顾一下配置文件:

app/router.js
module.exports = app => {
    const { router, controller } = app;
    router.get('/', controller.home.index);
    router.resources('users', '/users', controller.user);
};
config/plugin.js
module.exports = {
    validate: {
        enable: true,
        package: 'egg-validate',
    },
};
config/config.default.js
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 就能对接。以获取列表和新增为例:

user.js
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 就能处理所有接口,不用为每个接口写不同的解析逻辑。

前端页面如果跑在别的端口(比如 Vite 的 5173),浏览器会因跨域拦截请求。Egg 侧需要配置 CORS 允许跨域,真实项目中常用 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 里接收 pagepageSize 参数做数组切片,Controller 从 ctx.query 取参传进去,再配合上一篇练习里的 helper.page 返回即可。

Q4:这套结构能直接用到公司项目里吗?

分层结构和约定是可以直接借鉴的。但真实项目还需要数据库、登录鉴权、日志、参数更严格的校验等,这些是进阶内容,可以在此基础上逐步补齐。


🧪 小练习

给接口加分页功能:GET /users?page=1&pageSize=10,Service 中对数组切片,返回时用统一格式带上总数。

app/service/user.js
// 在 UserService 中新增方法
async findByPage(page, pageSize) {
    // 请在这里编写代码
}
app/controller/user.js
// 改造 index 方法
async index() {
    const { ctx } = this;
    // 请在这里编写代码
}

🎉 恭喜你完成了用户管理模块的实战,也走完了 Egg.js 课程的全部内容!从脚手架创建项目到分层架构、插件扩展,你已经具备了用 Egg.js 开发接口的完整能力。接下来可以尝试接入数据库(如 egg-mysql、egg-sequelize),把这套结构用在真实项目中。