☰
Ghostfolio 实战解读:NestJS 按功能模块组织代码的架构最佳实践
2026/10/2 16:21:37 网站建设 项目流程
  • 后端
  • 前端
  • 金融科技
  • 数据可视化

【免费下载链接】ghostfolio

Open Source Wealth Management Software. Angular + NestJS + Prisma + Nx + TypeScript 🤍

项目地址:https://gitcode.com/GitHub_Trending/gh/ghostfolio
点击查看免费下载

本文以开源财富管理软件 Ghostfolio(Angular + NestJS + Prisma + Nx + TypeScript)的 API 端为实例,系统解读 NestJS 架构中"按功能模块(Feature Modules)组织代码"这一核心最佳实践。读者将掌握:为什么按技术层(controllers/services/entities 各归一堆)组织是反模式、功能模块的标准目录结构与@Module写法、根模块如何组装、模块间如何通过imports/exports协作,以及共享基础设施层(guards/interceptors/filters)应如何安放。

一、为什么"按功能模块"组织是 CRITICAL 级最佳实践

在 NestJS 项目的 架构最佳实践规则 中,"Organize by Feature Modules" 被标记为impact: CRITICAL,理由是它能让新成员上手和功能开发提速 3~5 倍。

其核心主张是:

将应用拆分为若干自包含的功能模块,每个模块内部收纳与自身功能相关的 controller、service、entity/model 与 DTO;避免按技术层组织(把所有 controller 放一起、所有 service 放一起)。

按技术层组织的问题在于:一个"用户"功能被肢解到 controllers/、services/、entities/ 三个目录,新增一个字段需要同时跳转多个目录、牵扯多个文件;新人看代码时无法一眼看出"某个功能到底包含哪些代码"。而按功能模块组织后,每个功能的所有代码收敛在一个目录里,边界清晰、职责单一、可独立演进。

Ghostfolio 的 API 端(apps/api/src/app)正是这一实践的大型真实样板:access/、account/、activities/、admin/、auth/、portfolio/、user/、subscription/、symbol/…… 每个业务域一个目录,每个目录内自成体系。

二、反模式剖析:按技术层组织(Anti-Pattern)

规则文档给出的反面示例如下:

// Technical layer organization (anti-pattern) src/ ├── controllers/ │ ├── users.controller.ts │ ├── orders.controller.ts │ └── products.controller.ts ├── services/ │ ├── users.service.ts │ ├── orders.service.ts │ └── products.service.ts ├── entities/ │ ├── user.entity.ts │ ├── order.entity.ts │ └── product.entity.ts └── app.module.ts // Imports everything directly

这种结构的典型代价:

  • 功能被物理拆散:实现"订单"功能需要横跨三个顶层目录,改动涉及多文件、多位置;
  • 根模块臃肿:app.module.ts必须把所有 controller、service 一股脑 import 进来,模块边界形同虚设;
  • 依赖关系混乱:不同功能之间互相引用彼此的技术层文件,缺少显式的模块边界约束,容易形成意大利面式依赖;
  • 难以复用与替换:想整体替换"用户"功能时,必须从多个目录中逐一清理文件。

从源码结构看,Ghostfolio 完全没有采用这种组织方式:apps/api/src/app下不存在controllers/、services/、entities/这样的技术层目录,而是清一色的功能目录。

三、正确范式:功能模块的标准组织与 @Module 声明

规则文档给出了推荐的标准形态:

// Feature module organization src/ ├── users/ │ ├── dto/ │ │ ├── create-user.dto.ts │ │ └── update-user.dto.ts │ ├── entities/ │ │ └── user.entity.ts │ ├── users.controller.ts │ ├── users.service.ts │ ├── users.repository.ts │ └── users.module.ts ├── orders/ │ ├── dto/ │ ├── entities/ │ ├── orders.controller.ts │ ├── orders.service.ts │ └── orders.module.ts ├── shared/ │ ├── guards/ │ ├── interceptors/ │ ├── filters/ │ └── shared.module.ts └── app.module.ts

配套的模块声明代码(规则文档原样保留):

// users.module.ts @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService, UsersRepository], exports: [UsersService], // Only export what others need }) export class UsersModule {} // app.module.ts @Module({ imports: [ ConfigModule.forRoot(), TypeOrmModule.forRoot(), UsersModule, OrdersModule, SharedModule, ], }) export class AppModule {}

范式要点可归纳为三条:

  1. 功能内聚:dto/、entities/、controller、service、repository 全部归属同一模块目录;
  2. 边界显式:@Module通过controllers声明对外暴露的 HTTP 端点,通过providers声明内部可注入实例,通过imports声明依赖,通过exports精确控制"只导出别人需要的那部分";
  3. 共享层独立:跨模块复用的 guards、interceptors、filters 放入shared/,由SharedModule统一提供。

四、Ghostfolio 源码印证:apps/api/src/app 的真实目录结构

Ghostfolio 的 API 应用模块根目录 apps/api/src/app 是上述范式的直接落地。每个业务域一个目录,例如:

  • access/:共享访问权限管理(access.controller.ts、access.service.ts、access.module.ts)
  • account/:账户管理(account.controller.ts、account.service.ts、account.module.ts、get-all-accounts.dto.ts、interfaces/cash-details.interface.ts)
  • activities/:交易活动管理(controller、service、module、两个 DTO、一个 spec 测试)
  • admin/、auth/、auth-device/、exchange-rate/、export/、import/、health/、info/、logo/、platform/、portfolio/、redis-cache/、subscription/、symbol/、user/
  • endpoints/:按 API 用途继续细分为ai/、api-keys/、asset-profiles/、assets/、benchmarks/、data-providers/、market-data/、mcp/、platforms/、public/、sitemap/、tags/、watchlist/

与规则文档的范式一一对应:

  • 每个目录内同时包含controller、service、module(如account.controller.ts+account.service.ts+account.module.ts),而非把所有 controller 集中到一个目录;
  • DTO 与接口就近存放:account/get-all-accounts.dto.ts、account/interfaces/cash-details.interface.ts、activities/get-activities.dto.ts、activities/activities-filter.dto.ts;
  • 测试文件跟随模块存放,如activities/activities.service.spec.ts、portfolio/portfolio.service.spec.ts、portfolio/current-rate.service.spec.ts,便于功能级验证与单元测试定位;
  • 跨模块可复用的守卫、拦截器、过滤器、装饰器则上提到共享层:apps/api/src/guards/、apps/api/src/interceptors、apps/api/src/filters/、apps/api/src/decorators/,对应规则文档中的shared/层。

五、模块内部内聚:以 AccountModule 为例

看 apps/api/src/app/account/account.module.ts 的完整声明:

import { AccountBalanceModule } from '@ghostfolio/api/app/account-balance/account-balance.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { UserModule } from '@ghostfolio/api/app/user/user.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { ApiModule } from '@ghostfolio/api/services/api/api.module'; // ...(其余 imports 略) @Module({ controllers: [AccountController], exports: [AccountService], imports: [ AccountBalanceModule, ApiModule, ConfigurationModule, ExchangeRateDataModule, ImpersonationModule, PortfolioModule, PrismaModule, RedactValuesInResponseModule, TagModule, UserModule ], providers: [AccountService] }) export class AccountModule {}

对照规则文档的users.module.ts写法,可发现完全同构的三点:

  1. controllers: [AccountController]:该模块只暴露账户相关的 HTTP 端点;
  2. providers: [AccountService]:服务仅在本模块作用域内注册;
  3. exports: [AccountService]:只导出AccountService供其他模块(如ActivitiesModule、PortfolioModule)按需使用,而 controller、DTO、接口等内部细节对外不可见,实现"只导出别人需要的"。

Controller 层的内聚同样明显。以 apps/api/src/app/account/account.controller.ts 为例,AccountController只处理账户域的路由(@Controller('account')),并就地使用权限装饰器(@HasPermission、@RequiresScope)、拦截器(RedactValuesInResponseInterceptor、TransformDataSourceInRequestInterceptor)与 DTO(CreateAccountDto、UpdateAccountDto、TransferBalanceDto、GetAllAccountsDto)。DTO 校验也集中在同目录的 get-all-accounts.dto.ts 中——它继承FilterDto并通过 class-validator 的@IsOptional/@IsString/@MaxLength(SEARCH_QUERY_MAXIMUM_LENGTH)声明查询参数约束。这正是"每个功能模块自包含 controller、service、entities、DTO"的落地形态。

六、模块间协作:imports/exports 的依赖网络

功能模块之间通过imports/exports建立显式依赖。看两个典型例子:

ActivitiesModule(activities.module.ts)依赖了ApiModule、BenchmarkModule、DataProviderModule、ExchangeRateDataModule、MarketDataModule、PrismaModule、TagModule以及两个数据采集队列模块等十几个模块——它把"交易活动"这一业务域所需的数据访问、行情、汇率、标签、缓存能力全部以模块为单位装配进来,并提供ActivitiesService。

PortfolioModule(portfolio.module.ts)则集中了组合计算相关的一切:providers中注册PortfolioCalculatorFactory、CurrentRateService、PortfolioService、RulesService、AccountBalanceService、AccountService,exports仅导出PortfolioService,imports包含AccessModule、ActivitiesModule、BenchmarkModule、DataProviderModule、MarketDataModule、RedisCacheModule及多个拦截器模块。

从源码结构看,AccountModule与PortfolioModule之间存在相互引用(AccountModule imports PortfolioModule,而 PortfolioModule 的 providers 中直接提供 AccountService),这是 NestJS 生态中常见"模块间需要相互访问服务"时的处理方式之一——通过exports配合在目标模块 providers 中直接注入实现解耦。对于真实的循环依赖场景,NestJS 官方还提供forwardRef()机制,但这在 Ghostfolio 的模块声明中并未出现。

七、根模块 AppModule:只负责组装,不承载业务

功能模块全部就位后,apps/api/src/app/app.module.ts 只做一件事:把各功能模块与基础设施模块组装进imports数组。其真实代码(节选关键部分):

@Module({ controllers: [AppController], imports: [ AdminModule, AccessModule, AccountModule, ActivitiesModule, AiModule, ApiKeysModule, AssetProfilesModule, AssetModule, AssetsModule, AuthDeviceModule, AuthModule, BenchmarksModule, // ... BullBoard、Bull、Cache、Config、Cron、DataProvider 等基础设施模块 ExchangeRateModule, ExportModule, HealthModule, ImportModule, InfoModule, LogoModule, MarketDataModule, McpModule, PlatformModule, PortfolioModule, PrismaModule, PublicModule, SitemapModule, SubscriptionModule, SymbolModule, TagsModule, UserModule, WatchlistModule // ... ], providers: [ I18nService, { provide: APP_FILTER, useClass: PortfolioSnapshotComputationExceptionFilter }, { provide: APP_GUARD, useClass: ImpersonationWriteGuard } ] }) export class AppModule implements NestModule { public configure(consumer: MiddlewareConsumer) { consumer.apply(HtmlTemplateMiddleware).forRoutes('*wildcard'); } }

几点值得注意的实战细节:

  • 根模块只做注册:imports中列的是模块而非散落的类,业务逻辑全部留在功能模块内部;
  • 全局提供者通过令牌注入:APP_FILTER与APP_GUARD这种全局横切关注点,通过@nestjs/core的令牌在根模块声明,对应规则文档中"共享层放 guards/filters"的思想;
  • 跨模块中间件在根模块配置:AppModule implements NestModule通过configure()把HtmlTemplateMiddleware应用到全部路由,属于应用级装配而非业务逻辑;
  • TypeORM 被 Prisma 取代:规则文档示例中的TypeOrmModule.forRoot()在 Ghostfolio 中对应PrismaModule(apps/api/src/services/prisma),持久层实现不同,但"模块化组织"的骨架完全一致。

八、共享基础设施层:guards / interceptors / filters / decorators

规则文档要求把跨模块复用的守卫、拦截器、过滤器放入shared/。Ghostfolio 的对应实现位于 API 应用的顶层目录:

  • apps/api/src/guards:access.guard.ts、custom-throttler.guard.ts、has-permission.guard.ts、impersonation.guard.ts、oauth-callback.guard.ts、scope.guard.ts等,其中不少配有同名.spec.ts测试;
  • apps/api/src/interceptors:performance-logging/、redact-values-in-response/、transform-data-source-in-request/、transform-data-source-in-response/四个拦截器模块,被多个功能模块复用(如 AccountModule 引入RedactValuesInResponseModule,ActivitiesModule 同时引入请求/响应两个 Transform 模块);
  • apps/api/src/filters/:mcp-tool-exception.filter.ts、portfolio-snapshot-computation-exception.filter.ts(后者在根模块以APP_FILTER全局注册);
  • apps/api/src/decorators/:has-permission.decorator.ts、requires-scope.decorator.ts、impersonation.decorator.ts、allow-during-impersonation.decorator.ts等,功能模块 controller 中大量使用。

这套共享层与功能模块目录(app/)分层清晰:功能模块目录描述"业务有什么",共享层描述"横切能力有哪些",两者通过模块imports显式连接,而不是靠文件散落隐式共享。

九、落地自查清单

按照规则文档并结合 Ghostfolio 的实践,评估自己项目时可用以下清单逐项自查:

  1. 目录名 = 业务功能:apps/api/src/app下每个子目录是否都对应一个清晰业务域(access、account、activities、portfolio……),而不是controllers/、services/、entities/这类技术层目录;
  2. 模块自包含:每个功能目录内是否同时具备 controller、service、module,以及就近的 DTO 与 interfaces;
  3. exports 最小化:@Module的exports是否只列出其他模块真正需要的东西(如 AccountModule 只导出AccountService);
  4. 根模块轻量化:app.module.ts是否只负责imports组装与全局横切配置,不含业务实现;
  5. 共享层独立:guards、interceptors、filters、decorators 是否上提为共享目录并被各模块显式imports;
  6. 测试随模块:功能级 spec 是否与模块源码同目录存放(如activities/activities.service.spec.ts)。

十、小结

"按功能模块组织"之所以被列为 CRITICAL 级最佳实践,是因为它直接决定了大型 NestJS 项目的可维护性与协作效率。Ghostfolio 的 API 端(apps/api/src/app)用一套完整可运行的真实代码证明了这条规则的价值:每个业务域一个自包含目录,controller/service/DTO/接口就近内聚,模块通过@Module的imports/exports显式协作,共享守卫与拦截器独立成层,根模块只做装配。无论是从零搭建 NestJS 应用,还是重构既有"按技术层堆文件"的代码库,这套组织方式都值得作为默认架构基线。

  • 后端
  • 前端
  • 金融科技
  • 数据可视化

【免费下载链接】ghostfolio

Open Source Wealth Management Software. Angular + NestJS + Prisma + Nx + TypeScript 🤍

项目地址:https://gitcode.com/GitHub_Trending/gh/ghostfolio
点击查看免费下载

相关推荐

上一篇:Gutenberg `useConstrainedTabbing` Hook 完全指南:在弹窗与对话框内约束 Tab 焦点循环
下一篇:Potpie Resource Manager 架构解析:从 ADR-0004 到授权上下文租约(Authorized Context Lease)的实现

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询