一款基于 NestJS 的 DDD 脚手架,开箱即用

一款基于 NestJS 的 DDD 脚手架,开箱即用

一个基于 NestJS 10 + TypeORM 的领域驱动设计(DDD)Node.js 脚手架,内置双数据库、领域事件、统一响应、Swagger 文档与 Docker 一键启动示例。

这是什么

nestjs-ddd 是一个面向 Node.js / TypeScript 的 DDD 工程脚手架,帮你用几分钟时间搭好一个符合 DDD 分层规范的后端服务。

项目内置 用户(User)订单(Order) 两个示例聚合,用户库走 MySQL、订单库走 PostgreSQL,包含 15 个 REST 接口、领域事件发布与监听、Swagger 文档、Docker Compose 一键启动。

功能与 gin-ddd / springboot4ddd 对齐,便于对比不同语言栈在 DDD 工程中的落地差异。

DDD概念速览

DDD(领域驱动设计) 把软件按业务概念分层建模,让代码结构和业务知识保持一致。相比 MVC,DDD 把核心业务规则集中放在 领域层 里,而不是散落在 Controller / Service 的流程代码中。

四层结构

flowchart LR subgraph Layers["DDD 四层架构"] UI["接口层 Interfaces
HTTP / RPC 入口"] APP["应用层 Application
用例编排 · 事务边界"] DOMAIN["领域层 Domain
聚合根 · 业务规则 · 领域事件"] INFRA["基础设施层 Infrastructure
数据库 · 消息 · 缓存实现"] end UI --> APP APP --> DOMAIN APP -. 依赖接口 .-> INFRA INFRA -. 实现接口 .-> DOMAIN classDef ui fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef app fill:#534AB7,stroke:#3C3489,color:#ffffff,stroke-width:2px classDef domain fill:#D85A30,stroke:#993C1D,color:#ffffff,stroke-width:2px classDef infra fill:#BA7517,stroke:#854F0B,color:#ffffff,stroke-width:2px class UI ui class APP app class DOMAIN domain class INFRA infra style Layers fill:#F5F5F5,stroke:#CCCCCC,color:#333333
关注点 约束
接口层 HTTP 协议适配(VO / 路由 / 过滤器) 不写业务逻辑
应用层 用例编排、事务、DTO / Command 只依赖领域层
领域层 业务规则、聚合、领域事件、仓储接口 零框架依赖
基础设施层 TypeORM、消息、邮件等技术实现 向上实现领域接口

一句话:MVC 按技术切,DDD 按业务切。业务越复杂,DDD 的价值越明显。

为什么 Node.js 也要用 DDD?

有人会觉得:Node.js 是不是写写脚本、BFF 就够了,没必要用 DDD?

结论很简单:用不用 DDD 和语言无关,和业务复杂度有关。

  • 小项目 :NestJS 默认的 Module / Controller / Service 三件套就够用,没必要上 DDD。
  • 中大型项目 (模块超过 10 个、接口超过 50 个、业务规则密集),很容易出现:
    • Service 越写越臃肿,一个方法里掺杂参数校验、业务规则、事务、事件、ORM 细节;
    • 对象退化成只有 getter/setter 的"贫血模型",业务散落在各个 Service;
    • 新来的人看不懂代码结构在表达什么业务。
  • 这时候用 DDD 就能把代码按业务语义重新切清楚
    • 聚合根 持有业务规则,不再被 Service 侵蚀;
    • 应用服务 只负责编排和事务,职责非常薄;
    • 仓储接口 + 依赖倒置 让领域层完全独立于数据库/框架。
  • TypeScript + NestJS 特别适合落地 DDD:强类型表达值对象/聚合,装饰器 + DI 天然契合依赖倒置,几乎不用造轮子。

核心特点

  • 严格 DDD 四层架构domain / application / infrastructure / interfaces
  • NestJS 10 + TypeORM 0.3:现代企业级 Node.js 技术栈
  • 双数据库:MySQL(用户) + PostgreSQL(订单),TypeORM 多数据源独立管理
  • 领域事件 :基于 @nestjs/event-emitter 的内存事件总线,接口化可切换 Kafka / RabbitMQ / RocketMQ
  • 统一 API 契约 :snake_case JSON + { code, message, data } 响应格式
  • 自动响应包装 + 全局异常:Controller 只需返回 DTO,异常自动转换
  • Swagger 文档 :启动即可访问 http://localhost:8080/docs
  • YAML 配置:和 gin-ddd 对齐,支持多环境
  • Docker Compose:一条命令拉起 MySQL + PostgreSQL + App
  • 零业务侵入的参数校验class-validator 装饰器

技术栈

技术 版本 用途
Node.js 18+ 运行时
TypeScript 5.5 强类型
NestJS 10.x Web 框架
TypeORM 0.3.x ORM(多数据源)
MySQL 8.0+ 用户库默认
PostgreSQL 14+ 订单库默认
class-validator 0.14 DTO 校验
@nestjs/event-emitter 2.x 内存事件总线
@nestjs/swagger 7.x OpenAPI
js-yaml 4.x YAML 配置

工程结构

%%{init: {'flowchart': {'nodeSpacing': 10, 'rankSpacing': 50}}}%% flowchart TB subgraph IF["接口层 Interfaces"] Controller["UserController
OrderController"] Wrapper["异常过滤器 · 响应拦截器"] end subgraph APP["应用层 Application"] AppService["UserApplicationService
OrderApplicationService"] end subgraph DOMAIN["领域层 Domain"] Aggregate["User · Order 聚合根"] RepoIF["Repository 接口"] Event["领域事件"] PubIF["EventPublisher 接口"] end subgraph INFRA["基础设施层 Infrastructure"] RepoImpl["Repository 实现"] OrmEntity["TypeORM 实体"] PubImpl["InMemoryEventPublisher"] Listener["事件监听器"] MySQL[("MySQL
frog")] PG[("PostgreSQL
seed")] end Controller --> AppService AppService --> Aggregate AppService --> RepoIF AppService --> PubIF Aggregate -. 产生 .-> Event RepoIF -. 实现 .-> RepoImpl PubIF -. 实现 .-> PubImpl RepoImpl --> OrmEntity OrmEntity --> MySQL OrmEntity --> PG PubImpl -. 触发 .-> Listener classDef if fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef app fill:#534AB7,stroke:#3C3489,color:#ffffff,stroke-width:2px classDef domain fill:#D85A30,stroke:#993C1D,color:#ffffff,stroke-width:2px classDef infra fill:#BA7517,stroke:#854F0B,color:#ffffff,stroke-width:2px classDef db fill:#185FA5,stroke:#0C447C,color:#ffffff,stroke-width:2px classDef event fill:#993556,stroke:#72243E,color:#ffffff,stroke-width:2px class Controller,Wrapper if class AppService app class Aggregate,RepoIF domain class Event,PubIF event class RepoImpl,OrmEntity,PubImpl,Listener infra class MySQL,PG db style IF fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style APP fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style DOMAIN fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style INFRA fill:#F5F5F5,stroke:#CCCCCC,color:#333333

目录结构

bash 复制代码
nestjs-ddd/
├── config/
│   ├── config.yaml                  # 本地开发配置
│   └── config.docker.yaml           # Docker 环境配置
├── docs/
│   ├── init.mysql.sql               # 用户库 schema + 测试数据
│   └── init.postgres.sql            # 订单库 schema + 测试数据
├── Dockerfile                       # 多阶段构建
├── compose.yaml                     # 一键拉起 DB + App
├── src/
│   ├── domain/                      # 领域层(纯业务)
│   │   ├── shared/events/
│   │   ├── user/{model,repository}
│   │   └── order/{model,repository,events}
│   ├── application/                 # 应用层(用例编排)
│   │   ├── user/{dto,user-application.service.ts}
│   │   └── order/{dto,order-application.service.ts}
│   ├── infrastructure/              # 基础设施层(技术实现)
│   │   ├── config/
│   │   ├── persistence/{user,order}
│   │   └── messaging/
│   ├── interfaces/http/             # 接口层(HTTP 适配)
│   │   ├── common/ · filters/ · interceptors/
│   │   ├── user/ · order/
│   │   └── app.controller.ts
│   ├── modules/                     # NestJS 模块装配
│   ├── app.module.ts
│   └── main.ts
└── README.md

请求生命周期

以"创建订单"为例,展示请求在四层之间的流转:

flowchart TB subgraph Flow["POST /api/orders 完整链路"] direction TB C["客户端
发送 JSON"] --> V["接口层
ValidationPipe 校验 VO"] V --> Ctrl["OrderController
转换为 Command"] Ctrl --> App["应用层
OrderApplicationService.createOrder"] App --> Check["查询用户是否存在"] Check --> Agg["领域层
Order.create() 执行业务规则"] Agg --> Repo["基础设施层
OrderRepositoryImpl.save"] Repo --> DB[("PostgreSQL
INSERT orders")] DB --> Pub["基础设施层
EventPublisher.publish"] Pub --> Event["OrderCreatedEvent
异步触发监听器"] Event --> Resp["响应拦截器
包装 {code, message, data}"] Resp --> C end classDef client fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef if fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef app fill:#534AB7,stroke:#3C3489,color:#ffffff,stroke-width:2px classDef domain fill:#D85A30,stroke:#993C1D,color:#ffffff,stroke-width:2px classDef infra fill:#BA7517,stroke:#854F0B,color:#ffffff,stroke-width:2px classDef db fill:#185FA5,stroke:#0C447C,color:#ffffff,stroke-width:2px classDef event fill:#993556,stroke:#72243E,color:#ffffff,stroke-width:2px class C client class V,Ctrl,Resp if class App,Check app class Agg domain class Repo,Pub infra class DB db class Event event style Flow fill:#F5F5F5,stroke:#CCCCCC,color:#333333

快速开始

1. 环境准备

  • Node.js 18+ / npm 9+
  • MySQL 8.0+、PostgreSQL 14+(或直接用 docker compose

2. 初始化数据库(首次必做)

脚手架默认 synchronize: false(生产安全),需要先用 init SQL 建表:

bash 复制代码
mysql -u root -p < docs/init.mysql.sql

psql -U postgres -c "CREATE DATABASE seed;"
psql -U postgres -d seed -f docs/init.postgres.sql

两个脚本都是可重复执行的(内置 DROP TABLE IF EXISTS),跑过旧版本也能直接覆盖。

3. 安装依赖并启动

bash 复制代码
npm install
npm run start:dev

启动后访问:

  • API 前缀:http://localhost:8080/api
  • Swagger 文档http://localhost:8080/docs
  • 健康检查:http://localhost:8080/health

4. 一键 Docker 启动(推荐新手)

不想本地装数据库,直接:

bash 复制代码
docker compose up -d        # MySQL + PostgreSQL + App 全部起
docker compose logs -f app  # 观察启动日志
docker compose down -v      # 彻底清理

compose.yaml 会自动把 docs/*.sql 挂到容器的 /docker-entrypoint-initdb.d/,首次启动自动建表。

5. 试一下接口

bash 复制代码
# 创建用户
curl -X POST http://localhost:8080/api/users \
  -H 'Content-Type: application/json' \
  -d '{"name":"jarry","email":"jarry@example.com"}'

# 创建订单(注意 snake_case)
curl -X POST http://localhost:8080/api/orders \
  -H 'Content-Type: application/json' \
  -d '{"user_id":1,"total_amount":99.9}'

# 支付订单(状态机转换)
curl -X PUT http://localhost:8080/api/orders/1/pay

统一响应格式:

json 复制代码
{ "code": 200, "message": "success", "data": { "id": 1, "status": "PAID" } }

更多接口列表和开发细节见 NestJS-DDD-Development-Guide.md

事件驱动设计

领域层定义事件接口,基础设施层提供实现,业务代码只和接口打交道,这样就可以在不改业务的前提下,把事件总线从内存切到 Kafka / RocketMQ / RabbitMQ。

flowchart LR subgraph Source["事件产生"] App["OrderApplicationService"] end subgraph Abstract["事件抽象"] Bus["EventPublisher 接口"] end subgraph Impl["事件实现(可替换)"] Mem["内存总线
@nestjs/event-emitter"] Kafka["Kafka / RocketMQ / RabbitMQ"] end subgraph Handler["事件处理"] L1["OrderEventListener
日志记录"] L2["MailListener
发送邮件"] Consumer["外部消费者服务"] end App -- publish --> Bus Bus --> Mem Bus -. 可切换 .-> Kafka Mem --> L1 Mem --> L2 Kafka --> Consumer classDef app fill:#534AB7,stroke:#3C3489,color:#ffffff,stroke-width:2px classDef bus fill:#993556,stroke:#72243E,color:#ffffff,stroke-width:2px classDef impl fill:#BA7517,stroke:#854F0B,color:#ffffff,stroke-width:2px classDef listener fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef mq fill:#185FA5,stroke:#0C447C,color:#ffffff,stroke-width:2px class App app class Bus bus class Mem impl class Kafka mq class L1,L2 listener class Consumer mq style Source fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style Abstract fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style Impl fill:#F5F5F5,stroke:#CCCCCC,color:#333333 style Handler fill:#F5F5F5,stroke:#CCCCCC,color:#333333

订单事件:order.created / order.paid / order.shipped / order.delivered / order.cancelled / order.refunded

订单状态机

订单的业务规则用状态机表达,全部封装在 Order 聚合根内部,外部只能通过 pay() / ship() / deliver() 等方法触发合法转换:

flowchart LR subgraph StateMachine["订单状态机"] direction LR PENDING(["PENDING
待支付"]) PAID(["PAID
已支付"]) SHIPPED(["SHIPPED
已发货"]) DELIVERED(["DELIVERED
已送达"]) CANCELLED(["CANCELLED
已取消"]) REFUNDED(["REFUNDED
已退款"]) end PENDING -- pay --> PAID PENDING -- cancel --> CANCELLED PAID -- ship --> SHIPPED PAID -- refund --> REFUNDED SHIPPED -- deliver --> DELIVERED SHIPPED -- refund --> REFUNDED classDef pending fill:#BA7517,stroke:#854F0B,color:#ffffff,stroke-width:2px classDef active fill:#534AB7,stroke:#3C3489,color:#ffffff,stroke-width:2px classDef success fill:#1D9E75,stroke:#0F6E56,color:#ffffff,stroke-width:2px classDef terminal fill:#993556,stroke:#72243E,color:#ffffff,stroke-width:2px class PENDING pending class PAID,SHIPPED active class DELIVERED success class CANCELLED,REFUNDED terminal style StateMachine fill:#F5F5F5,stroke:#CCCCCC,color:#333333

和 gin-ddd / springboot4ddd 对比

维度 gin-ddd (Go) springboot4ddd (Java) nestjs-ddd (TS)
Web 框架 Gin Spring Boot 4 NestJS 10
ORM GORM JdbcTemplate / Spring Data TypeORM
IoC 手工装配 Spring NestJS DI
事件总线 RocketMQ RocketMQ event-emitter(可换)
参数校验 binding tag Jakarta Validation class-validator
API 文档 springdoc-openapi @nestjs/swagger
启动 go run mvn spring-boot:run npm run start:dev

数据库 schema / API 契约 / 业务规则三端完全对齐,可以直接用同一份 Postman 集合跨语言调用,非常适合对比学习。希望通过这几款DDD脚手架对比,能助你加深对于DDD的理解和运营。谢谢!

相关链接

相关推荐
@不误正业2 小时前
第09章-分布式硬件平台
分布式·架构·开源·开源鸿蒙
StackNoOverflow2 小时前
SpringCloud 声明式服务调用 —— Feign 全面解析(入门 + 原理 + 优化)
后端·spring·spring cloud
fy121632 小时前
Spring Boot spring-boot-maven-plugin 参数配置详解
spring boot·后端·maven
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第3题:ArrayList和LinkedList有什么区别
java·开发语言·后端·面试·list
jzwugang2 小时前
SpringBoot + vue 管理系统
vue.js·spring boot·后端
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第4题:LinkedList是单向链表还是双向链表
java·开发语言·数据结构·后端·链表·面试·list
Liu.7742 小时前
Vue 3开发中遇到的报错(1)
前端·javascript·vue.js
Victor3563 小时前
MongoDB(95)如何在MongoDB中使用加密存储引擎?
后端
Victor3563 小时前
MongoDB(96)如何使用MongoDB的高级聚合功能?
后端