如何在Fastify中拆分controllers与services以实现业务逻辑解耦复用?
Fastify 拆分 Controller 与 Service 实现方案
核心设计思路
- Service 层完全不依赖 Fastify 相关 API,只处理纯业务逻辑,入参出参都是普通数据类型,可单独抽离复用甚至给其他项目使用
- Controller 层只做参数校验、请求响应格式处理、调用 Service、返回响应,不编写具体业务逻辑
目录结构参考
src ├── app.js # Fastify 实例初始化、注册路由 ├── controllers # 控制器层 │ └── user.controller.js ├── services # 业务逻辑层 │ └── user.service.js └── schemas # 可选,存放接口校验 schema
代码示例
1. Service 层实现(完全无框架依赖)
以用户相关业务为例,你也可以不用类,把每个业务逻辑写成独立函数导出,效果一致:
// src/services/user.service.js class UserService { // 模拟数据库存储 #users = [ { id: 1, name: '张三', email: 'zhangsan@example.com' }, { id: 2, name: '李四', email: 'lisi@example.com' } ] async getUserById(userId) { // 纯业务逻辑,和请求响应完全解耦 if (!userId || typeof userId !== 'number') { throw new Error('用户 ID 格式错误') } const user = this.#users.find(u => u.id === userId) if (!user) { throw new Error('用户不存在') } // 可扩展其他业务逻辑:数据脱敏、关联查询等 return { id: user.id, name: user.name, email: user.email } } async createUser(userData) { const { name, email } = userData if (!name || !email) { throw new Error('用户名和邮箱不能为空') } const existedUser = this.#users.find(u => u.email === email) if (existedUser) { throw new Error('该邮箱已被注册') } const newUser = { id: this.#users.length + 1, name, email } this.#users.push(newUser) return newUser } } // 导出单例即可 module.exports = new UserService()
如果后续需要迁移到 Koa、Express 等其他框架,Service 层代码可以直接复用,不需要任何修改
2. Controller 层实现(仅处理请求响应相关逻辑)
// src/controllers/user.controller.js const userService = require('../services/user.service') // 获取用户详情接口控制器 async function getUserController(request, reply) { try { // 仅从请求中提取参数,业务逻辑完全交给 Service 处理 const { userId } = request.params const user = await userService.getUserById(Number(userId)) return reply.code(200).send({ code: 0, message: '查询成功', data: user }) } catch (err) { return reply.code(400).send({ code: 1, message: err.message }) } } // 创建用户接口控制器 async function createUserController(request, reply) { try { const userData = request.body const newUser = await userService.createUser(userData) return reply.code(201).send({ code: 0, message: '创建成功', data: newUser }) } catch (err) { return reply.code(400).send({ code: 1, message: err.message }) } } module.exports = { getUserController, createUserController }
3. 路由注册
// src/app.js const fastify = require('fastify')({ logger: true }) const { getUserController, createUserController } = require('./controllers/user.controller') // 路由直接绑定对应控制器 fastify.get('/users/:userId', getUserController) fastify.post('/users', createUserController) // 启动服务 const start = async () => { try { await fastify.listen({ port: 3000 }) } catch (err) { fastify.log.error(err) process.exit(1) } } start()
扩展优化点
- 如果需要在 Service 中调用数据库、第三方接口等依赖,建议通过依赖注入的方式传入,不要在 Service 内部硬编码实例化,后续更换依赖、写单元测试时会更方便
- 复杂项目可以再加一层 Repository 专门处理数据库 CURD,Service 层仅做业务编排,拆分粒度更细
- 可以用 Fastify 装饰器把 Service 实例挂载到 Fastify 实例上,不用每个 Controller 都手动导入,注意不要在 Service 中调用 Fastify 实例属性即可
内容的提问来源于stack exchange,提问作者Jenya7771
相关产品推荐
相关产品推荐

