1. 项目概述:从脚手架到架构生成器的进化

如果你和我一样,常年混迹在Node.js后端开发的一线,肯定对“项目初始化”这件事又爱又恨。爱的是,一个干净、标准的项目结构是后续一切美好开发体验的基石;恨的是,每次新建项目,从安装依赖、配置ESLint、Prettier、Jest,到搭建基础的目录分层、连接数据库、配置环境变量……这一套标准化的“起手式”流程,既繁琐又容易出错。更别提,当团队里来了新人,或者你隔了几个月再回头看自己的老项目,发现每个项目的结构都略有不同,那种维护和理解的酸爽,懂的都懂。

这就是为什么,从几年前开始,我就一直在维护一个内部使用的Node.js项目快速启动工具。最初的版本,我们叫它“Quickstart Generator”,本质上就是一个增强版的 create-react-app 或者 vue-cli ,但更贴合我们自己的技术栈(Express/Koa + TypeScript + Prisma/Sequelize)。它的核心功能很简单:通过命令行交互,选择你需要的数据库、ORM、测试框架、代码风格工具,然后一键生成一个配置好的项目骨架。这个工具在团队内部节省了大量时间,也统一了项目规范。

然而,随着微服务、领域驱动设计(DDD)这些理念在我们团队越来越深入,我们逐渐发现,仅仅生成一个“能跑”的项目骨架是远远不够的。我们真正需要的,是一个具备清晰架构、职责分离、易于测试和长期维护的代码基座。于是,从去年开始,我们着手对老工具进行了一次彻底的重构和升级,目标就是让它从一个“脚手架”进化成一个“架构生成器”。今天要跟大家分享的,就是这个历时近一年打磨的 Node.js Quickstart Generator v2.0.0

v2.0.0的核心升级,可以概括为两点: “Automated Clean Architecture” “Sleek New UI” 。前者是灵魂,它意味着工具现在能自动为你生成一个遵循“整洁架构”(Clean Architecture)原则的项目结构,将业务逻辑、框架、数据库等关注点清晰地分离开;后者是皮囊,我们彻底重写了命令行交互界面,让它更直观、更友好,甚至能实时预览你选择不同配置后生成的项目结构图。简单来说,现在你只需要回答几个问题,就能得到一个生产就绪、架构清晰、配置完善的Node.js后端项目,开箱即用,直接开始写业务逻辑。

2. 核心设计理念:为什么是“整洁架构”?

在深入v2.0.0的具体功能之前,我觉得有必要先花点篇幅聊聊我们为什么选择“整洁架构”(Clean Architecture, 也叫“洋葱架构”)作为这次升级的核心设计范式。这不是为了赶时髦,而是我们在经历了无数个“祖传代码”项目后的血泪教训。

2.1 传统分层架构的痛点

在v1.0时代,我们生成的是经典的三层架构:Controller(控制层)、Service(服务层)、Model(数据模型层)。这个模式在项目初期简单明了,但随着业务复杂度的指数级增长,问题开始暴露:

  1. 框架依赖症 :业务逻辑(Service层)里充斥着对Express req res 对象的直接操作,或者对Prisma Client的强依赖。一旦你想把Web框架从Express换成Fastify,或者把ORM从Prisma换成TypeORM,几乎意味着Service层需要推倒重来。
  2. 数据库绑架业务 :业务规则经常与特定的数据表结构、SQL查询语句紧密耦合。改变数据库设计或更换数据库类型(比如从PostgreSQL迁移到MySQL)变得异常困难。
  3. 测试困难 :因为业务逻辑和框架、IO(数据库、外部API)紧密绑定,单元测试需要大量模拟(Mock),测试用例既难写又脆弱,一个框架版本的升级可能就让一堆测试挂掉。
  4. 可读性下降 :一个Service函数里,可能同时包含了参数校验、权限判断、业务计算、数据库操作、日志记录、消息推送等所有事情,违反了单一职责原则,代码像一团意大利面。

2.2 整洁架构的核心优势

罗伯特·C·马丁(Bob大叔)提出的整洁架构,其核心思想是 依赖关系规则 :源代码中的依赖关系必须只指向同心圆的内层,即由内向外。内层圆代表高层策略(业务逻辑),外层圆代表底层细节(框架、数据库、UI)。

应用到我们的Node.js后端项目,通常可以划分为四个同心圆层(从内到外):

  • 实体(Entities) :封装企业范围的业务规则,是最纯粹、最稳定的部分。例如“用户”、“订单”这些核心业务对象及其基本验证规则。
  • 用例(Use Cases) :封装应用特定的业务规则。它协调数据流向实体和从实体流出,并指挥外层适配器去完成工作。例如“创建用户”、“处理订单支付”。
  • 接口适配器(Interface Adapters) :将数据从最适合用例和实体的格式,转换为最适合外层(如数据库、Web框架)的格式。这里包含控制器(Controllers)、网关(Gateways)、持久层接口(Repositories Interfaces)等。
  • 框架与驱动(Frameworks & Drivers) :最外层,是所有的具体实现细节:Web框架(Express)、数据库(Prisma/TypeORM)、外部服务API客户端等。

这种架构带来的直接好处是:

  • 框架无关性 :业务核心(实体、用例)不关心你用Express还是Koa,甚至不关心你是不是Web应用。它们只是纯粹的JavaScript/TypeScript对象和函数。
  • 数据库无关性 :业务逻辑通过接口(抽象)与数据库交互,具体是用Prisma还是手写SQL,是在MySQL还是MongoDB里存,都是外层可替换的细节。
  • 可测试性 :因为核心业务逻辑独立于所有外部因素,你可以用最简单的Jest进行单元测试,无需启动Web服务器或连接真实数据库。
  • 长期可维护性 :当技术栈更新换代时,你只需要更换最外层“适配器”的具体实现,而核心业务代码稳如泰山。

v2.0.0的“Automated Clean Architecture”,就是帮你自动搭建好这样一个分层清晰、依赖方向明确的项目骨架,并生成符合各层职责的样板代码。

3. v2.0.0 功能全景与实操解析

好了,理论铺垫完毕,我们进入实战环节。来看看v2.0.0这个工具具体能为你做什么,以及如何使用它。

3.1 全新交互式命令行界面(Sleek New UI)

首先,安装工具。我们将其发布到了npm全局包。

npm install -g node-quickstart-generator
# 或者使用 yarn
yarn global add node-quickstart-generator

安装完成后,在任意目录下执行:

create-node-app

这时,你会看到与旧版本截然不同的启动界面。我们抛弃了之前单调的文字列表,采用了一个支持方向键导航、高亮显示、分组提示的增强型交互界面(基于 inquirer chalk 深度定制)。

界面主要分为几个配置区块:

  1. 项目基本信息 :项目名称、描述、版本、作者等。
  2. 技术栈选择
    • 运行时 :Node.js版本(自动检测并推荐LTS版本)。
    • 包管理器 :npm, yarn, pnpm 三选一。
    • 语言 :TypeScript(强烈推荐)或 JavaScript。
    • Web框架 :Express, Koa, Fastify。选择后,后续的控制器、中间件生成都会适配对应框架。
  3. 架构与代码风格
    • 架构模板 :这是核心选项。除了新的“Clean Architecture”模板,我们保留了经典的“MVC”模板和更简单的“REST API”模板,以适应不同复杂度的项目。
    • 代码风格 :集成ESLint( Airbnb / Standard / 自定义规则)和Prettier,一键配置。
  4. 数据库与ORM
    • 数据库类型 :PostgreSQL, MySQL, SQLite, MongoDB。
    • ORM/ODM :Prisma(对SQL数据库),Mongoose(对MongoDB),TypeORM(通用)。选择会联动,例如选了MongoDB,ORM选项会自动聚焦到Mongoose。
    • 数据库连接与迁移工具 :自动生成 .env 文件模板、数据库连接配置模块、以及Prisma的 schema.prisma 或Mongoose的Schema模型。
  5. 测试与质量
    • 测试框架 :Jest(默认)或 Mocha/Chai。
    • 测试覆盖度报告 :集成 jest-coverage nyc
    • API测试 :可选生成基于Supertest的API集成测试样板。
  6. 高级功能
    • 认证授权 :可选集成JWT(JSON Web Token)认证的样板代码,包括用户注册、登录、刷新令牌、保护路由的中间件。
    • API文档 :可选集成Swagger/OpenAPI 3.0,自动根据路由生成API文档界面。
    • 容器化 :一键生成 Dockerfile docker-compose.yml 文件,方便本地开发和部署。
    • CI/CD模板 :可选生成GitHub Actions或GitLab CI的配置文件模板,用于自动化测试和部署。

提示 :在交互过程中,你可以随时按“空格键”预览当前选择所生成的项目目录树状图。这个预览是动态的,能让你在生成前就对项目结构有直观把握,避免配置错误。

完成所有选择后,工具会让你确认最终配置。确认后,它便会开始执行以下自动化操作:

  1. 创建项目目录并初始化 package.json
  2. 安装所有你选择的依赖(开发依赖和生产依赖)。
  3. 根据“整洁架构”模板,生成完整的目录结构和样板文件。
  4. 配置ESLint、Prettier、Jest、TypeScript、环境变量等。
  5. 如果选择了数据库,会生成数据库连接配置、模型定义和种子数据脚本。
  6. 如果选择了高级功能,会生成对应的模块和配置。
  7. 最后,初始化Git仓库,并执行第一次提交(提交信息为“Initial commit from Node.js Quickstart Generator v2.0.0”)。

整个过程无需人工干预,喝杯咖啡的功夫,一个架构清晰、配置完备的Node.js项目就诞生了。

3.2 生成的“整洁架构”项目结构详解

让我们深入看看,当你选择“Clean Architecture”模板后,生成的项目根目录是什么样子。这是理解工具价值的关键。

my-awesome-project/
├── src/
│   ├── core/                    # 核心业务逻辑(内层)
│   │   ├── domain/             # 领域层(实体与值对象)
│   │   │   ├── entities/       # 业务实体(如 User, Product)
│   │   │   │   └── user.entity.ts
│   │   │   ├── value-objects/  # 值对象(如 Email, Money)
│   │   │   └── exceptions/     # 领域异常
│   │   └── application/        # 应用层(用例)
│   │       ├── use-cases/      # 用例(如 CreateUserUseCase)
│   │       │   └── create-user.use-case.ts
│   │       ├── ports/          # 端口(接口/抽象)
│   │       │   ├── repositories/ # 仓储接口
│   │       │   │   └── user.repository.port.ts
│   │       │   └── services/    # 外部服务接口(如支付、邮件)
│   │       └── dto/            # 应用层数据传输对象
│   ├── infrastructure/          # 基础设施层(外层-具体实现)
│   │   ├── http/               # Web框架相关
│   │   │   ├── controllers/    # 控制器(实现端口,调用用例)
│   │   │   │   └── user.controller.ts
│   │   │   ├── middleware/     # 中间件(认证、日志、错误处理)
│   │   │   └── routes/         # 路由定义
│   │   ├── persistence/        # 持久化相关
│   │   │   ├── databases/      # 数据库配置与连接
│   │   │   ├── repositories/   # 仓储具体实现(如 UserPrismaRepository)
│   │   │   └── migrations/     # 数据库迁移(如Prisma迁移目录)
│   │   └── external-services/  # 外部服务客户端实现
│   ├── shared/                 # 共享资源
│   │   ├── utils/              # 通用工具函数
│   │   └── config/             # 应用配置(环境变量加载等)
│   └── main.ts                 # 应用入口,组装所有依赖
├── tests/                      # 测试目录(结构与src镜像)
├── .env.example                # 环境变量示例
├── .eslintrc.js               # ESLint配置
├── .prettierrc                # Prettier配置
├── jest.config.js             # Jest配置
├── tsconfig.json              # TypeScript配置
├── package.json
└── README.md                   # 自动生成的项目说明

各目录职责解读:

  • src/core/domain :这是业务的“心脏”。 entities 里是纯粹的领域对象,只有属性和验证自身状态的方法,没有任何外部依赖。 user.entity.ts 里定义的 User 类,只知道自己的 id name email 以及 validatePassword 这样的方法。
  • src/core/application/use-cases :这里是具体的业务操作流程。例如 CreateUserUseCase 类,它的 execute 方法会接收一个输入DTO,调用 User 实体进行验证,然后通过 UserRepository 端口(接口)去保存。 关键点 :这个类只依赖于 UserRepository 这个接口,而不是具体的Prisma或Mongoose。
  • src/core/application/ports :这是内外层通信的“契约”。 UserRepository.port.ts 里定义了一个接口,比如 save(user: User): Promise<User> 。内层的用例依赖这个接口,外层的具体实现(在 infrastructure 里)来实现这个接口。
  • src/infrastructure/persistence/repositories :这里是适配器的具体实现。 UserPrismaRepository 类实现了 UserRepository 接口,内部使用Prisma Client来操作数据库。如果明天要换TypeORM,你只需要新建一个 UserTypeOrmRepository 类实现同一个接口,然后在入口处替换注入即可,核心业务代码一行不用改。
  • src/infrastructure/http/controllers :控制器负责处理HTTP请求。它从请求中提取数据,转换成用例需要的DTO,调用对应的用例,然后将用例返回的结果转换成HTTP响应。它很“薄”,只做适配工作。
  • src/main.ts :这是依赖注入的“组装车间”。在这里,你创建所有具体实现的实例(如 UserPrismaRepository ),并将它们注入到用例中(如 new CreateUserUseCase(userRepository) ),最后启动Web服务器。

通过这样的结构,依赖方向非常清晰: 控制器 -> 用例 -> 实体 ,并且用例只依赖抽象的端口(接口),具体实现从外部注入。这完美符合了整洁架构的依赖倒置原则。

3.3 核心环节实现:以一个用户注册API为例

让我们通过一个具体的用户注册流程,来看看生成的项目中代码是如何协作的。假设我们选择了Express、TypeScript、Prisma和PostgreSQL。

1. 领域实体 ( src/core/domain/entities/user.entity.ts ):

// 这是一个纯粹的业务对象,没有外部依赖
export class User {
  constructor(
    public readonly id: string,
    public name: string,
    public email: string,
    private passwordHash: string,
    public readonly createdAt: Date = new Date()
  ) {}

  // 业务规则:验证密码
  validatePassword(inputPassword: string, hashFunc: (pw: string) => string): boolean {
    return this.passwordHash === hashFunc(inputPassword);
  }

  // 业务规则:邮箱格式验证(简单示例)
  static isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

2. 仓储端口 ( src/core/application/ports/repositories/user.repository.port.ts ):

// 这是一个接口,定义数据存取契约
import { User } from '../../../domain/entities/user.entity';

export interface UserRepository {
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>;
  // ... 其他方法
}

3. 用例 ( src/core/application/use-cases/create-user.use-case.ts ):

import { User } from '../../domain/entities/user.entity';
import { UserRepository } from '../ports/repositories/user.repository.port';

// 用例的输入DTO
export interface CreateUserInput {
  name: string;
  email: string;
  password: string;
}

// 用例本身
export class CreateUserUseCase {
  constructor(private readonly userRepository: UserRepository) {}

  async execute(input: CreateUserInput): Promise<User> {
    // 1. 验证输入(可使用class-validator等,这里简化)
    if (!User.isValidEmail(input.email)) {
      throw new Error('Invalid email format');
    }

    // 2. 检查用户是否已存在(业务规则)
    const existingUser = await this.userRepository.findByEmail(input.email);
    if (existingUser) {
      throw new Error('User with this email already exists');
    }

    // 3. 创建领域实体(这里模拟密码哈希,实际应使用bcrypt)
    const passwordHash = `hashed_${input.password}`; // 简化
    const user = new User(
      `generated-id-${Date.now()}`, // 实际应由仓库或数据库生成ID
      input.name,
      input.email,
      passwordHash
    );

    // 4. 通过抽象端口保存实体
    const savedUser = await this.userRepository.save(user);
    return savedUser;
  }
}

4. 基础设施层:Prisma仓储实现 ( src/infrastructure/persistence/repositories/user-prisma.repository.ts ):

import { User } from '../../../core/domain/entities/user.entity';
import { UserRepository } from '../../../core/application/ports/repositories/user.repository.port';
import { PrismaClient } from '@prisma/client';

export class UserPrismaRepository implements UserRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findByEmail(email: string): Promise<User | null> {
    const userData = await this.prisma.user.findUnique({ where: { email } });
    if (!userData) return null;
    // 将数据库模型转换为领域实体
    return new User(
      userData.id,
      userData.name,
      userData.email,
      userData.passwordHash,
      userData.createdAt
    );
  }

  async save(user: User): Promise<User> {
    const userData = await this.prisma.user.create({
      data: {
        // ID如果是数据库自增,这里可能需要调整
        name: user.name,
        email: user.email,
        passwordHash: user['passwordHash'], // 注意:这里访问了私有字段,实际中需通过getter或方法
      },
    });
    // 返回更新后的实体(包含数据库生成的ID和日期)
    return new User(
      userData.id,
      userData.name,
      userData.email,
      userData.passwordHash,
      userData.createdAt
    );
  }
}

5. 基础设施层:Express控制器 ( src/infrastructure/http/controllers/user.controller.ts ):

import { Request, Response } from 'express';
import { CreateUserUseCase, CreateUserInput } from '../../../core/application/use-cases/create-user.use-case';

export class UserController {
  constructor(private readonly createUserUseCase: CreateUserUseCase) {}

  async createUser(req: Request, res: Response): Promise<void> {
    try {
      const input: CreateUserInput = req.body;
      const newUser = await this.createUserUseCase.execute(input);
      // 将领域实体转换为HTTP响应DTO(通常不会直接返回密码哈希)
      res.status(201).json({
        id: newUser.id,
        name: newUser.name,
        email: newUser.email,
        createdAt: newUser.createdAt,
      });
    } catch (error) {
      // 错误处理,可以映射领域错误到HTTP状态码
      res.status(400).json({ error: (error as Error).message });
    }
  }
}

6. 依赖组装与启动 ( src/main.ts ):

import express from 'express';
import { PrismaClient } from '@prisma/client';
import { UserPrismaRepository } from './infrastructure/persistence/repositories/user-prisma.repository';
import { CreateUserUseCase } from './core/application/use-cases/create-user.use-case';
import { UserController } from './infrastructure/http/controllers/user.controller';

async function bootstrap() {
  const app = express();
  app.use(express.json());

  // 1. 初始化具体的外部依赖(适配器)
  const prisma = new PrismaClient();
  const userRepository = new UserPrismaRepository(prisma);

  // 2. 创建用例,并注入具体实现
  const createUserUseCase = new CreateUserUseCase(userRepository);

  // 3. 创建控制器,并注入用例
  const userController = new UserController(createUserUseCase);

  // 4. 定义路由
  app.post('/users', (req, res) => userController.createUser(req, res));

  // 5. 启动服务器
  app.listen(3000, () => {
    console.log('Server is running on http://localhost:3000');
  });
}

bootstrap().catch(console.error);

通过这个完整的链条,你可以清晰地看到请求的流动:HTTP请求 -> 控制器(适配) -> 用例(业务逻辑) -> 实体(业务规则) -> 仓储接口(抽象) -> Prisma仓储(具体实现) -> 数据库。每一层职责单一,依赖清晰,无论是替换框架、更换数据库,还是进行单元测试,都变得轻而易举。

4. 高级功能与集成指南

v2.0.0不仅仅生成骨架,还集成了许多现代Node.js开发所需的“开箱即用”功能,这些功能都设计为可插拔模块,与核心架构无缝集成。

4.1 认证授权(JWT)模块

如果你在初始化时选择了“Authentication (JWT)”选项,工具会额外生成以下内容:

  • src/core/domain/entities/token.entity.ts :定义令牌相关的值对象。
  • src/core/application/use-cases/auth/ :包含 LoginUseCase (登录)、 RegisterUseCase (注册,可能复用创建用户)、 RefreshTokenUseCase (刷新令牌)等用例。
  • src/core/application/ports/services/token.service.port.ts :定义令牌生成、验证的抽象接口。
  • src/infrastructure/services/jwt-token.service.ts :使用 jsonwebtoken 库实现上述接口。
  • src/infrastructure/http/middleware/auth.middleware.ts :Express中间件,用于保护需要认证的路由。它会从请求头中提取JWT,通过 TokenService 验证,并将解码后的用户信息(如userId)注入到 req.user 中。
  • 相应的控制器和路由。

集成关键点 :认证逻辑被封装为用例,令牌服务是可通过端口替换的适配器。这意味着你可以轻松地将JWT换成Paseto或其他令牌方案,只需实现新的 TokenService 即可。

4.2 API文档(Swagger/OpenAPI)集成

选择“API Documentation (Swagger)”后,工具会:

  1. 安装 swagger-jsdoc swagger-ui-express
  2. src/infrastructure/http/swagger/ 目录下生成Swagger配置和初始化代码。
  3. 在控制器的方法上,自动生成JSDoc注释,其中包含 @swagger 标签,描述API的路径、方法、参数、响应体等。
  4. main.ts 中设置Swagger UI路由(如 /api-docs )。

实操心得 :我们刻意将Swagger注释放在控制器这一“适配器”层,而不是用例层。因为API文档是HTTP协议这一“交付机制”的细节,与核心业务逻辑无关。这样保持了用例的纯净性。

4.3 容器化与生产就绪配置

生成的 Dockerfile 是一个多阶段构建的优化版本,适用于Node.js应用:

# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build # 如果是TypeScript项目

# 运行阶段
FROM node:18-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist # 或 lib, 根据构建输出目录
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]

同时生成的 docker-compose.yml 通常包含了应用服务和一个数据库服务(如PostgreSQL),并配置了网络和卷,方便一键启动完整开发环境。

version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    environment:
      - DATABASE_URL=postgresql://user:password@db:5432/mydb
    depends_on:
      - db
    volumes:
      - ./:/app # 开发时挂载代码卷, 生产环境应移除
      - /app/node_modules

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

5. 常见问题、排查技巧与避坑指南

在实际使用和推广v2.0.0的过程中,我和团队遇到了不少问题,也积累了一些经验。这里分享几个最常见的坑和解决方法。

5.1 依赖注入(DI)与循环依赖

在整洁架构中,我们需要在 main.ts (或一个专门的 composition-root.ts )中手动组装所有依赖。当项目规模变大,依赖关系变复杂时,手动 new new 去会变得混乱,也容易产生循环依赖。

解决方案 : 我们 没有 在生成器中强制引入一个完整的IoC容器(如 tsyringe inversify ),以保持项目的轻量和灵活性。但我们提供了两种模式:

  1. 简单项目模式 :如上面的示例,在 main.ts 中直接组装。适用于中小型项目。
  2. 高级项目模式 (可选):在初始化时选择“Advanced DI Setup”,工具会生成一个 src/infrastructure/di/container.ts 文件,使用一个轻量级的容器(如我们封装的一个简单 Container 类)来管理依赖。它会自动扫描 use-cases repositories ,进行注册和解析,简化 main.ts 的代码。

循环依赖排查 :如果遇到 Cannot access before initialization 这类错误,通常是模块导入顺序问题。确保你的依赖方向是单向的(外层依赖内层)。TypeScript的 import 语句是静态的,如果A文件导入了B,B又导入了A,就会形成循环。解决方法是使用依赖注入将依赖作为参数传递,或者将共享代码提取到第三个文件中。

5.2 领域实体与持久化模型的映射

这是整洁架构中一个经典的“阻抗不匹配”问题。领域实体是富含行为的业务对象,而数据库模型(如Prisma生成的 User 模型)通常是纯粹的数据结构。在仓储实现中,我们需要进行两者之间的转换。

生成器提供的模式

  • UserPrismaRepository 中,我们手动编写转换逻辑(如上面示例中的 new User(...) )。这种方式清晰直接。
  • 对于复杂对象(如聚合根包含多个实体),我们生成了 toDomain() toPersistence() 的静态方法样板,放在仓储里或单独的映射器(Mapper)类中。

避坑技巧

  • 不要将ORM模型直接暴露给领域层 :坚决禁止在实体或用例中导入 @prisma/client 的类型。这违反了依赖规则。
  • 小心处理私有字段 :如上例中, User 实体的 passwordHash 是私有的。在仓储中转换时,可能需要一个公共的getter方法(如 getPasswordHash() )来获取,或者重新思考该字段是否应该完全私有。这是一个设计权衡点。
  • 使用工厂函数 :对于复杂的实体创建逻辑,可以在实体类中提供静态工厂方法,如 User.create(props) ,将验证和初始化逻辑封装在里面,使仓储代码更简洁。

5.3 测试策略与Mock

整洁架构最大的优势之一就是可测试性。我们的生成器为每一层都生成了对应的测试样板。

  • 领域实体测试 ( tests/core/domain/... ) : 纯单元测试,无需任何外部依赖。直接用Jest测试实体的方法和业务规则。
    describe('User Entity', () => {
      it('should validate correct email', () => {
        expect(User.isValidEmail('test@example.com')).toBe(true);
        expect(User.isValidEmail('invalid-email')).toBe(false);
      });
    });
    
  • 用例测试 ( tests/core/application/use-cases/... ) : 单元测试,需要Mock掉所有端口依赖。我们使用Jest的 jest.mock 自动模拟了整个端口模块,或者手动创建Mock对象。
    describe('CreateUserUseCase', () => {
      let mockUserRepo: jest.Mocked<UserRepository>;
      let useCase: CreateUserUseCase;
    
      beforeEach(() => {
        mockUserRepo = {
          findByEmail: jest.fn(),
          save: jest.fn(),
        };
        useCase = new CreateUserUseCase(mockUserRepo);
      });
    
      it('should create a user successfully', async () => {
        mockUserRepo.findByEmail.mockResolvedValue(null);
        mockUserRepo.save.mockResolvedValue(/* 一个模拟的用户实体 */);
    
        await expect(useCase.execute(validInput)).resolves.not.toThrow();
        expect(mockUserRepo.save).toHaveBeenCalledTimes(1);
      });
    });
    
  • 控制器/适配器测试 ( tests/infrastructure/http/... ) : 集成测试。使用Supertest来测试完整的HTTP请求/响应流,但Mock掉用例层。因为控制器只负责适配,逻辑简单,所以测试重点在HTTP状态码、响应格式和错误处理上。
  • 仓储实现测试 ( tests/infrastructure/persistence/... ) : 这类测试比较特殊。理想情况下,你应该用一个真实的内存数据库(如SQLite)或一个测试数据库来测试。我们建议对仓储实现进行集成测试,以确保其与数据库的交互是正确的。生成器会配置一个单独的测试数据库连接。

测试配置 :工具生成的 jest.config.js 已经配置好了模块路径映射(与 tsconfig.json 中的 paths 对应),并设置了 testEnvironment 和覆盖度报告。对于需要数据库的测试,我们推荐使用 jest globalSetup globalTeardown 来启动和销毁一个独立的测试数据库实例。

5.4 项目演进与目录结构调整

生成的项目结构是一个推荐的起点,但不是金科玉律。随着业务增长,你可能会发现 use-cases 目录下的文件过多,或者 domain/entities 需要按子域(Bounded Context)进一步拆分。

我们的建议

  • 初期 :严格遵守生成的结构,这有助于建立团队共识。
  • 中期 :当某个领域(如 order )变得复杂时,可以考虑将其相关的实体、值对象、用例、仓储接口移动到一个单独的模块或目录中,例如 src/core/order/ 。这依然是整洁架构,只是物理结构的重组。
  • 后期 :当系统演变为多个微服务时,每个微服务都可以使用这个生成器独立初始化,并拥有自己独立的领域模型。服务间通过API或消息队列通信。

工具生成的结构提供了清晰的边界和依赖方向,只要不违反“内层不依赖外层”的核心规则,内部的目录组织可以根据项目的实际情况灵活调整。

从v1.0的简单脚手架到v2.0的架构生成器,这个工具的进化本质上反映了我们团队对软件质量、可维护性和开发体验的持续追求。它不仅仅是一个节省初始化时间的工具,更是一个将最佳实践和架构思想“固化”到项目起点的载体。对于个人开发者,它能让你快速搭建一个经得起时间考验的项目基础;对于团队,它是统一技术栈、降低新人上手成本、保障代码质量的利器。当然,没有银弹,整洁架构会带来一定的前期复杂度和学习成本,但对于任何预期有长期生命力和复杂度的Node.js后端项目来说,这份投入绝对是值得的。希望这个工具和它背后的设计思路,能对你的下一个项目有所启发。

更多推荐