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

Node.js后端开发:Service/Controller层、业务逻辑与项目结构咨询

Node.js 后端分层架构实践(适配Express生态技术栈)

以下实践完全对齐Java EE/Spring生态成熟的分层设计原则,遵循职责分离、接口隔离、可测试的核心要求,适配你当前使用的Express、Socket.io、RabbitMQ、Passport.js、Sequelize/Mongoose技术栈,也兼容后续GraphQL接入场景。


一、分层边界与职责划分

可以直接映射你熟悉的Spring生态分层逻辑,各层严格遵守单一职责,绝对不跨层写逻辑:

  • 路由层:仅做请求与处理逻辑的绑定,对应Spring MVC中@RequestMapping的注册逻辑,职责包括:Express路由注册、Socket.io事件监听绑定、后续GraphQL的Schema与Resolver绑定。路由层绝对不能承担Controller职责,仅可挂载全局中间件(跨域、Body解析、Passport鉴权),不要在路由回调中写参数处理、业务调用、响应组装逻辑,否则后续做HTTP/Socket/GraphQL多协议适配时,同一份业务逻辑会重复写三遍,和早年JSP嵌Scriptlet、Servlet里硬写JDBC的问题完全一致。
  • Controller层:必须独立拆分,和Spring MVC的Controller职责完全等价,仅做三件事:解析对应协议的上下文参数(HTTP请求体/查询参数、Socket连接上下文与事件Payload、Passport注入的req.user信息、GraphQL入参)、调用对应Service处理业务、捕获业务异常后组装统一格式的响应/错误返回。Controller层不写任何业务规则、不直接操作数据库。
  • Service层:是唯一存放业务逻辑的位置,对应Spring的Service层,所有业务规则校验、数据库事务控制、跨Model的数据操作、RabbitMQ消息投递/消费编排、邮件/日志等通用能力的调用组合,全部放在Service层。Service层要做到和协议完全解耦,不感知上层是HTTP、Socket还是GraphQL,不直接引用Express的req/res对象,保证逻辑可复用。
  • Repository/DAO层:对应Spring Data JPA/MyBatis Mapper层,仅做单表/单集合的CRUD封装,Sequelize的Model定义、Mongoose的Schema与基础查询方法放在这一层,不要把跨表/跨集合的业务逻辑写在Model的实例/静态方法里,跨域数据逻辑全部下沉到Service层。

二、Service层的实现方式选择

类写法、高阶函数(工厂函数)写法都可以,没有本质的优劣差异,核心要求只有两点:依赖可注入、与框架解耦,不要在Service文件里硬编码导入数据库连接、MQ客户端等外部依赖,避免测试时无法Mock。

类写法(对齐Java编码习惯)

通过构造函数注入依赖,和Spring构造注入的逻辑完全一致,适合习惯面向对象编码的团队:

// services/user.service.js
export class UserService {
  // 所有依赖通过构造函数传入,测试时可直接传入Mock对象
  constructor({ userRepo, mqClient, mailClient }) {
    this.userRepo = userRepo;
    this.mqClient = mqClient;
    this.mailClient = mailClient;
  }

  async createUser(createDto) {
    // 业务逻辑:校验用户名重复、密码加密、创建用户、发通知邮件、投递MQ消息
    const existUser = await this.userRepo.findOne({ username: createDto.username });
    if (existUser) throw new Error('用户名已存在');
    const user = await this.userRepo.create({
      ...createDto,
      password: hashPassword(createDto.password)
    });
    await this.mailClient.sendRegisterMail(user.email);
    await this.mqClient.sendToQueue('user.registered', { userId: user.id });
    return user;
  }
}

高阶函数(工厂函数)写法

通过函数参数注入依赖,返回封装了业务方法的对象,适合偏好函数式编码的团队,可测试性和类写法完全一致:

// services/user.service.js
export function createUserService({ userRepo, mqClient, mailClient }) {
  return {
    async createUser(createDto) {
      // 业务逻辑和类写法完全一致
    }
  }
}

不要为了追求“写法新潮”强行选择不熟悉的编码方式,只要满足依赖注入要求,两种写法的可维护性、可测试性没有区别。


三、Mocha友好的可测试代码规范

完全对齐JUnit/Spring Test的测试思路,核心是让业务逻辑和外部依赖解耦,不需要启动完整服务就能跑单元测试:

  • 所有层的依赖全部通过构造函数/工厂函数参数传入,不要在业务代码里硬编码导入数据库实例、MQ客户端等外部依赖,测试时直接传入Sinon生成的Stub/Mock对象即可,不需要连接真实数据库、不需要启动Express服务就能跑Service层测试。
  • Controller层单元测试仅校验三个点:参数解析是否正确、是否调用了对应Service的正确方法、返回的响应格式/状态码是否符合预期,Service层逻辑全部Mock,不需要跑真实业务流程。
  • Service层是单元测试的核心,覆盖所有业务分支、异常场景,所有外部依赖(Repository、MQ、邮件客户端)全部Mock,单测运行速度可以达到毫秒级,和Java中单元测试Service的逻辑完全一致。
  • 集成测试单独存放,启动测试环境的数据库、MQ实例,通过真实HTTP请求/Socket事件触发全链路流程,对应Spring Boot的集成测试逻辑。

Mocha测试Service的示例代码:

// test/unit/services/user.service.test.js
import { expect } from 'chai';
import sinon from 'sinon';
import { UserService } from '../../../services/user.service.js';

describe('UserService', () => {
  describe('createUser', () => {
    it('用户名已存在时抛出业务错误', async () => {
      const mockUserRepo = {
        findOne: sinon.stub().resolves({ id: 1, username: 'test' }),
        create: sinon.stub()
      };
      const userService = new UserService({
        userRepo: mockUserRepo,
        mqClient: { sendToQueue: sinon.stub() },
        mailClient: { sendRegisterMail: sinon.stub() }
      });

      try {
        await userService.createUser({ username: 'test', password: '123456' });
        expect.fail('应当抛出业务错误');
      } catch (err) {
        expect(err.message).to.equal('用户名已存在');
        expect(mockUserRepo.create.called).to.equal(false);
      }
    });
  });
});

四、适配当前技术栈的注意事项

  • Passport.js的鉴权逻辑全部封装在策略实现里,鉴权通过后的用户信息统一挂在req.user/Socket连接上下文上,Controller直接读取即可,不要在Service层处理鉴权逻辑,保证Service的无状态性。
  • Socket.io事件处理、后续GraphQL的Resolver直接复用同一套Controller/Service逻辑,不要单独编写业务代码,仅在Controller层做不同协议的参数适配即可。
  • Sequelize/Mongoose的Model仅做数据结构定义和单表基础查询,跨表关联查询、数据一致性逻辑全部放在Service层实现。
  • 业务逻辑统一抛自定义业务错误,由Controller层捕获后转换为对应协议的错误格式,不要在Service里直接操作响应对象,避免和框架强绑定。

内容的提问来源于stack exchange,提问作者Jerome Guenon

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 09:51:47