为什么是模块化单体
“模块化单体”不是把所有代码写在一个大目录,也不是未来无法拆分的临时方案。它表示:一个应用在运行和部署上保持单体,但内部按照稳定业务领域划分模块,并通过明确依赖控制模块之间的耦合。
四个容易混淆的概念
Monorepo
一个 Git 仓库同时管理多个应用和共享包。它解决版本、依赖、构建和协作问题,不决定运行时是否单体。
多应用
admin-api 与 api 是两个独立进程,可以分别部署。它们的认证对象和暴露接口不同,因此应用边界是合理的。
模块化单体
每个 NestJS 应用内部按 auth、user、role、menu、dictionary 等领域组织模块。模块通过显式 import 和服务接口协作,共享一个进程和主要数据库事务边界。
微服务
业务能力通过网络拆成自治服务,通常还需要网关、服务发现、配置管理、容错、幂等、消息、可观测性和独立发布治理。只有多个独立进程,并不代表这些问题已经得到解决。
为什么默认选择模块化单体
对个人开发者和小型团队,最稀缺的通常不是“能拆多少服务”,而是能否稳定完成:
- 数据模型与业务规则;
- 前后端契约;
- 登录认证与权限;
- 自动测试与部署;
- 线上故障定位和数据恢复。
模块化单体保留清晰代码边界,同时避免过早承担网络调用、分布式一致性和复杂运维成本。它并不是降低工程要求,反而要求模块职责、依赖方向和数据库变更更加明确。
什么时候继续放在 apps/api
满足大部分以下条件时,优先在同一应用增加领域模块:
- 与现有业务一起发布没有明显风险;
- 使用同一数据库事务更简单正确;
- 团队规模较小,领域由同一组人维护;
- 性能瓶颈可以通过索引、缓存、队列或多实例解决;
- 故障不会要求独立熔断和隔离。
什么时候创建额外服务
使用 pnpm new:server 的理由应来自真实约束:
- 需要独立发布节奏;
- 负载模型差异显著,需要独立扩缩容;
- 故障必须与核心 API 隔离;
- 数据或合规边界要求独立运行;
- 团队已经形成稳定自治边界。
创建服务只完成了代码和运行骨架,不会自动生成微服务治理能力。跨服务通信、认证传播、幂等、超时、重试、观测和数据一致性仍需单独设计。
推荐的演进顺序
- 先把领域拆成清晰 NestJS 模块。
- 用真实指标定位数据库、CPU、外部调用或任务处理瓶颈。
- 优先补索引、缓存、队列、限流、监控和多实例部署。
- 明确独立边界的收益高于网络与运维成本后再拆服务。
- 拆分时同时设计数据所有权、调用失败和发布回滚,而不是只移动目录。
因此,LY Fullstack 提供的是可持续演进的起点,不宣称一套默认结构可以覆盖所有规模。