人人都会AI编程

17.1 后端分层架构设计

更新时间:2026-07-10

在 Node.js 项目度过最初的“能跑就行”阶段后,随着接口增多、业务逻辑堆积,app.js(或 index.js)里动辄上千行的代码很快就会变得难以维护。一个合理的后端项目结构,本质上是在按照职责拆分代码,让每个文件只做一件事,且可以被替换和测试。这就是分层架构的价值。

目前 Node.js 社区最常见的分层方式是 三层架构控制器层(Controller)服务层(Service)数据访问层(DAO/Repository),并辅以中间件层、工具层、常量层等横切关注点。这套结构不依赖特定框架,无论是 Express、Koa 还是 NestJS,都可以在其上映射。

17.1.1 核心三层的职责与边界

1. 控制器层(Controller)

控制器是请求的第一个入口,它的唯一职责是解析 HTTP 请求参数、调用对应的服务层逻辑、组装 HTTP 响应。控制器不应该包含任何业务判断,更不应直接操作数据库。

// user.controller.js
const userService = require('./user.service');

exports.getUserById = async (req, res, next) => {
  try {
    const { id } = req.params;
    // 参数解析与校验
    if (!id) {
      return res.status(400).json({ message: '缺少用户ID' });
    }
    // 调用服务层
    const user = await userService.findUserById(id);
    if (!user) {
      return res.status(404).json({ message: '用户不存在' });
    }
    // 组装响应
    res.json({ data: user });
  } catch (err) {
    next(err);
  }
};

一个好的控制器函数应该很短,典型的职责包括:

  • req 中获取参数、路径、查询字符串、请求体
  • 调用 service 层的一个或多个方法
  • 根据返回结果构造 HTTP 状态码和 JSON 响应体
  • 不直接访问数据库,不拼接 SQL/ORM 语句
  • 不做复杂的业务判断(例如“用户是否有权限”应交给 service)

2. 服务层(Service)

服务层是业务逻辑的集中所在地。它从控制器接收已解析的参数,执行具体的业务规则,调用数据访问层获取或持久化数据,最后将结果返回给控制器。服务层并不知道 HTTP 的存在,它的输入输出都是普通的 JavaScript 对象。

// user.service.js
const userDao = require('./user.dao');

exports.findUserById = async (id) => {
  // 业务规则:例如用户状态校验
  const user = await userDao.getById(id);
  if (user && user.status === 'banned') {
    throw new Error('用户已被禁用');
  }
  // 可能还需要组合其他数据源
  return user;
};

服务层的典型特征:

  • 函数命名体现业务意图:registerUsertransferBalancecalculatePrice
  • 可能会调用多个 DAO 或外部 API 的组合
  • 负责事务、缓存、权限判断等业务规则的实现
  • 与通信协议无关,因此可以被单元测试直接调用,不需要启动 HTTP 服务

3. 数据访问层(DAO/Repository)

这一层的唯一任务是与数据源交互,提供读写数据的操作方法。数据源通常是数据库(MySQL、MongoDB),也可能是 Redis、搜索引擎、外部 HTTP 服务等。每个 DAO 通常对应一张表或一个数据集合。

// user.dao.js
const db = require('../config/database');

exports.getById = async (id) => {
  const [rows] = await db.execute('SELECT * FROM users WHERE id = ?', [id]);
  return rows[0] || null;
};

如果使用 ORM(如 Sequelize、TypeORM、Prisma),DAO 层会直接封装 ORM 的操作,并可以添加一些简单查询封装(如分页默认值、软删除过滤等)。关键点:

  • 不处理业务逻辑,只负责存取
  • 提供清晰的接口,服务层无需知道底层用的是 MySQL 还是 MongoDB
  • 便于后续更换数据库时集中修改

17.1.2 三层之外的横切关注点

除了纵向的三层,一个成熟的 Node.js 后端还需要处理横切关注点(Cross-cutting Concerns),它们不归属于某一层,而是贯穿请求处理的全过程。

中间件层(Middleware)

中间件是 Node.js Web 框架(特别是 Express/Koa)的核心机制,适合处理与请求生命周期相关的通用逻辑:

  • 请求日志(morgan、pino-http)
  • 跨域 CORS
  • 身份认证与鉴权(JWT 验证、Session 恢复)
  • 请求限流(express-rate-limit)
  • 请求体解析(express.json、multer)
  • 响应压缩(compression)

这些中间件应该在路由处理(控制器)之前或之后挂载,保持控制器代码的纯净。

工具层(Utils/Helpers)

纯粹的、无状态的工具函数集合,例如:

  • 日期格式化(dayjs 封装)
  • 加密解密函数(AES、MD5)
  • 随机字符串生成、唯一 ID 生成
  • 对象深拷贝、数组去重等通用操作

这些函数应该可以被任何层直接调用,且不依赖于项目状态。将它们集中放置,可以避免重复实现,也便于单元测试。

常量层(Constants)

项目中所有硬编码的常量都应该集中管理,例如:

  • HTTP 状态码常量
  • 错误消息枚举
  • 用户角色枚举
  • 订单状态常量
  • 配置项键名

这不仅提高代码可读性,也让全局修改(如修改错误提示文案)变得容易。通常建议按业务域划分常量文件,如 constants/error-code.jsconstants/user-status.js

异常处理层(Error Handling)

统一的错误处理流程也是分层的重要体现。通常做法:

  • 自定义业务异常类(如 AppError),携带状态码和错误消息
  • 服务层抛出自定义异常
  • 控制器或全局错误处理中间件捕获异常,转换为 HTTP 错误响应

这样业务逻辑产生的错误可以干净地传递到外层,而不必在每个控制器里写重复的 try-catch。

17.1.3 典型的项目目录结构

结合以上分层,一个常见的 Express 项目目录可能如下:

project/
├── src/
│   ├── controllers/      # 控制器层
│   │   ├── user.controller.js
│   │   └── order.controller.js
│   ├── services/         # 服务层
│   │   ├── user.service.js
│   │   └── order.service.js
│   ├── dao/              # 数据访问层
│   │   ├── user.dao.js
│   │   └── order.dao.js
│   ├── middlewares/      # 中间件
│   │   ├── auth.js
│   │   ├── error-handler.js
│   │   └── request-logger.js
│   ├── utils/            # 工具函数
│   │   ├── hash.js
│   │   └── validator.js
│   ├── constants/        # 常量定义
│   │   ├── error-code.js
│   │   └── user-role.js
│   ├── config/           # 配置文件(数据库、环境变量)
│   ├── routes/           # 路由定义(组装中间件与控制器)
│   └── app.js            # 应用入口,挂载中间件与路由
├── tests/                # 测试文件,结构与 src 对应
├── package.json
└── .env

注意:路由文件可以单独抽出一层,或者由控制器导出一组路由(如 NestJS 的装饰器路由),但其本质仍是绑定 URL 到控制器。

17.1.4 分层带来的实际收益

1. 可测试性
服务层和 DAO 层都是纯逻辑,不依赖 HTTP 请求对象,可以直接用 Jest 或 Vitest 进行单元测试,不需要启动服务。控制器也可以 mock 掉 service 来测试接口行为。

2. 可替换性
如果将来决定把 MySQL 换成 PostgreSQL,只需修改 DAO 层的实现,服务层和控制器层完全无感。同理,把 Express 换成 Fastify,控制器中的 req, res 处理需要调整,但服务层依然可以复用。

3. 团队协作清晰
新人接手可以快速定位某段代码属于哪一层,进而知道该层应该关注什么输入输出。代码评审时也可以按层级检查责任是否符合规范。

4. 复用性增强
服务层的函数可以被多个控制器调用(例如用户服务可被 web 端、管理后台、定时任务共用),避免了同样的业务逻辑在多处重复。

17.1.5 实践中避免过度分层

分层架构不是强制的教条。对于小型项目或原型,全栈框架的“单文件模块”可能更高效,不必为了分层而分层。一个好的判断标准是:当某个功能有超过两个地方的相似代码,或者单文件超过 200 行,就可以考虑拆分。始终以可维护性和可测试性作为最终目标,而不是机械地套用目录结构。

此外,分层后不应出现“穿越层”的调用。例如控制器直接访问 DAO,或者服务层直接返回 HTTP 状态码给控制器,这都是违反分层原则的,长期会破坏架构的清晰度。在代码评审中应持续留意这些越界行为。

合理的后端分层,能够让 Node.js 项目从“脚本堆砌”升级为“可持续维护的应用”。在接下来的小节中,我们将继续探讨 RESTful API 设计规范以及代码质量保障工具,使这套分层结构在实际开发中更加稳固。