You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 10:45:03