
1. 多租户多数据库在 NestJS 里到底难在哪NestJS 多租户多数据库架构设计与实现核心要解决的是同一套代码如何按租户身份把请求路由到不同数据库并且让 TypeORM、Prisma、Mongoose 三种 ORM 共存而不互相打架。适合正在做 SaaS 后台、需要按客户隔离数据、又不想为每个租户单独部署一套服务的后端开发者。我试过把三种 ORM 塞进一个 NestJS 项目最直观的感受是数据库连接本身不难难的是请求进来时怎么知道该用哪个连接以及AI 辅助编码时怎么让工具稳定读到这套配置。传统单库架构下所有租户共用一张表靠 tenant_id 字段做逻辑隔离。数据量一上来查询性能、备份恢复、合规审计都会变成瓶颈。多库架构每个租户独立数据库能解决隔离问题但带来三个新麻烦连接数量膨胀、配置管理分散、跨库事务无法用单库事务兜底。更现实的问题是当你在 NestJS 里同时接入 TypeORM 和 Prisma两者的连接生命周期、实体注册方式、注入 token 完全不同如果不在架构层做统一抽象业务代码里会到处出现 if-else 判断当前租户用哪个 ORM。这篇要给的是一套可复用的配置骨架用 DatabaseModule 集中管理三类 ORM 的公共模块用 x-tenant-id 请求头驱动数据源选择用 UserRepository 做统一门面。同时把 AI 编码工具的接入配置settings.json 与 config.toml一并给出让 Claude Code、Cline 这类工具在读写这套多租户代码时不会因为配置缺失而反复报错。整个链路通过 TaoToken 统一 Key 完成一次真实请求验证确认配置骨架能跑通。2. 前置准备TaoToken 统一 Key 与项目依赖在动手写多租户代码之前先把 AI 工具链的接入配置固定下来。这一步的意义在于多租户多数据库的代码量不小靠手写容易漏掉注入 token 或模块导入用 AI 辅助生成时如果 API 通道不稳定改到一半断掉会非常难受。TaoToken 提供统一的 API 通道一个 Key 可以覆盖模型对话、代码补全等场景省去在多个平台之间切换配置的麻烦。先到官网注册并创建 API Key地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 登录后在控制台生成 Key。API 基础地址统一用 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为 base_url 填入工具配置即可。项目依赖方面NestJS 侧需要安装npm i nestjs/common nestjs/core nestjs/config nestjs/typeorm typeorm npm i nestjs/mongoose mongoose npm i prisma/client npm i -D prismaPrisma 还需要初始化 schemanpx prisma init --datasource-provider mysql这一步会生成 prisma/schema.prisma后面多租户场景下每个租户的 Prisma Client 要单独生成所以 schema 里的 datasource url 先用环境变量占位不要写死。AI 工具侧如果你用的是 Claude Code 或 Cline需要配置 settings.json如果用 Codex 类 CLI则配置 config.toml。下面两节给出可直接复制的骨架。3. 可复制配置settings.json 与 config.toml 骨架3.1 settings.json 配置骨架这个文件通常放在用户目录下的工具配置目录中用于声明模型通道和 API 地址。核心是把 base_url 指向 TaoToken 的 API 地址api_key 填你创建的 Key。{ env: { ANTHROPIC_BASE_URL: https://taotoken.net/api, ANTHROPIC_API_KEY: sk-你的TaoToken密钥, ANTHROPIC_MODEL: claude-sonnet-4-20250514 }, permissions: { allow: [ Read, Write, Bash(npm run *), Bash(npx prisma *), Bash(npx tsc *) ] }, includeCoAuthoredBy: false }这里把 npm run、prisma、tsc 三类命令加入白名单是因为多租户项目里频繁需要重新生成 Prisma Client、跑类型检查、启动不同租户的测试服务。如果不放行AI 每次执行都要弹确认效率会掉得厉害。3.2 config.toml 配置骨架Codex 类 CLI 工具用 TOML 格式结构略有不同但本质一样声明 provider、base_url、api_key 和默认模型。model claude-sonnet-4-20250514 model_provider taotoken [model_providers.taotoken] name TaoToken base_url https://taotoken.net/api env_key TAOTOKEN_API_KEY [history] persistence save-all [tools] web_search false配置完成后把 TAOTOKEN_API_KEY 写入系统环境变量或者在启动 CLI 前 export 一次。注意 base_url 后面不要加 /v1 之类的路径TaoToken 的 API 地址已经包含了路由前缀多加反而会 404。3.3 多租户环境变量骨架回到 NestJS 项目本身租户连接信息用 .env 管理。下面这套命名规则和后面的 database.utils.ts 是配套的前缀 T_ 加租户标识再拼数据库字段。# Prisma 租户 T_PRISMA1_DATABASE_URLmysql://root:123456_mysqllocalhost:13306/testdb T_PRISMA2_DATABASE_URLpostgresql://pguser:123456_postgresqllocalhost:15432/testdb # TypeORM 租户 T_TYPEORM1_DB_TYPEmysql T_TYPEORM1_DB_HOSTlocalhost T_TYPEORM1_DB_PORT13306 T_TYPEORM1_DB_USERNAMEroot T_TYPEORM1_DB_PASSWORD123456_mysql T_TYPEORM1_DB_DATABASEtestdb T_TYPEORM2_DB_TYPEpostgres T_TYPEORM2_DB_HOSTlocalhost T_TYPEORM2_DB_PORT15432 T_TYPEORM2_DB_USERNAMEpguser T_TYPEORM2_DB_PASSWORD123456_postgresql T_TYPEORM2_DB_DATABASEtestdb # Mongoose 租户 T_MONGOOSE1_MONGODB_URImongodb://root:123456_mongodblocalhost:27018/nestmongodb T_MONGOOSE2_MONGODB_URImongodb://root:123456_mongodblocalhost:27019/nestmongodb注意Prisma 的 DATABASE_URL 在生成 Client 时需要保留一个默认值多租户的 URL 用 T_ 前缀区分生成脚本里按租户循环执行 prisma generate 并指定不同的 schema 输出目录。4. 核心实现租户解析与三类 ORM 统一门面4.1 租户解析工具请求进来后第一件事是从 header 里取出 x-tenant-id判断它属于哪个 ORM、对应哪个数据库前缀。这个逻辑集中在 database.utils.ts 里避免散落到各个 service。import { IncomingHttpHeaders } from http; import { tenantMap as prismaTenantMap, defaultTenant as prismaDefaultTenant } from ./prisma/prisma.constants; import { tenantMap as typeormTenantMap, defaultTenant as typeormDefaultTenant } from ./typeorm/typeorm.constants; import { tenantMap as mongooseTenantMap, defaultTenant as mongooseDefaultTenant } from ./mongoose/mongoose.constants; export function getTenantPrefix(headers: IncomingHttpHeaders) { let tenantId (headers[x-tenant-id] as string) || ; tenantId tenantId.toUpperCase(); const isPrisma prismaTenantMap.has(tenantId); const isTypeorm typeormTenantMap.has(tenantId); const isMongoose mongooseTenantMap.has(tenantId); if (!tenantId || !(isPrisma || isTypeorm || isMongoose)) { throw new Error(invalid tenantId); } let prefix ; if (isPrisma) prefix prismaTenantMap.get(tenantId) || prismaDefaultTenant; if (isTypeorm) prefix typeormTenantMap.get(tenantId) || typeormDefaultTenant; if (isMongoose) prefix mongooseTenantMap.get(tenantId) || mongooseDefaultTenant; return { isPrisma, isTypeorm, isMongoose, tPrefix: prefix, tenantId: tenantId.toLowerCase() }; }三个 tenantMap 分别在各自 ORM 的 constants 文件里定义例如 prisma.constants.tsexport const tenantMap new Mapstring, string([ [PRISMA1, T_PRISMA1], [PRISMA2, T_PRISMA2], ]); export const defaultTenant T_PRISMA1;4.2 统一 Repository 门面业务层不应该关心当前租户用的是哪种 ORM。UserRepository 作为门面注入三种适配器运行时按租户选择。import { REQUEST } from nestjs/core; import { Inject } from nestjs/common; import { Request } from express; import { getTenantPrefix } from /database/database.utils; import { UserAdapter } from ./user.interfaces; export class UserRepository implements UserAdapter { constructor( Inject(REQUEST) private request: Request, private userPrismaRepository: UserPrismaRepository, private userMongooseRepository: UserMongooseRepository, private userTypeOrmRepository: UserTypeOrmRepository, ) {} private getRepository(): UserAdapter { const { isPrisma, isTypeorm, isMongoose } getTenantPrefix(this.request.headers); if (isPrisma) return this.userPrismaRepository; if (isTypeorm) return this.userTypeOrmRepository; if (isMongoose) return this.userMongooseRepository; throw new Error(no matched repository); } find(): Promiseany[] { return this.getRepository().find(); } create(userObj: any): Promiseany { return this.getRepository().create(userObj); } }这里用到了策略模式加适配器模式UserAdapter 定义统一接口三个 Repository 各自实现getRepository 根据租户上下文选策略。Inject(REQUEST) 让每次请求都能拿到独立的 header不会出现租户串号。4.3 TypeORM 多连接注册TypeORM 的多租户连接用 dataSourceFactory 做缓存同一个租户第二次请求直接复用已初始化的 DataSource避免重复建连。const connections new Mapstring, DataSource(); Module({ imports: [ TypeOrmModule.forRootAsync({ name: TYPEORM_DB_CLIENT, useClass: TypeOrmConfigService, dataSourceFactory: async (options) { const tenantId options?.[tenantId] ?? ; if (tenantId connections.has(tenantId)) { return connections.get(tenantId)!; } const dataSource await new DataSource(options!).initialize(); connections.set(tenantId, dataSource); return dataSource; }, }), ], providers: [ TypeormProvider, { provide: TYPEORM_CONNECTIONS, useValue: connections }, ], }) export class TypeormCommonModule {}TypeOrmConfigService 里根据租户前缀从 process.env 读取对应的 DB_HOST、DB_PORT 等字段拼成 TypeORM 的 DataSourceOptions。Prisma 和 Mongoose 的公共模块结构类似只是配置服务的读取字段不同。5. 验证请求一次跑通多租户数据源切换配置写完后启动服务并用 curl 验证。先确认 AI 工具通道正常再验证多租户路由。5.1 验证 TaoToken 通道用 curl 直接打一次模型对话接口确认 Key 和 base_url 配置正确curl --request POST \ --url https://taotoken.net/api/v1/messages \ --header content-type: application/json \ --header x-api-key: sk-你的TaoToken密钥 \ --header anthropic-version: 2023-06-01 \ --data { model: claude-sonnet-4-20250514, max_tokens: 64, messages: [{role: user, content: reply with ok}] }返回内容里出现正常的文本响应说明通道可用。如果返回 401检查 Key 是否复制完整返回 404检查 base_url 是否多写了路径。5.2 验证多租户数据源切换启动 NestJS 服务后依次请求不同租户curl --request GET \ --url http://localhost:3000/user/multi \ --header x-tenant-id: prisma1预期返回 Prisma 租户 1 对应 MySQL 库里的用户数据。换成 typeorm2 再请求一次curl --request GET \ --url http://localhost:3000/user/multi \ --header x-tenant-id: typeorm2这次应该命中 TypeORM 租户 2 的 PostgreSQL 库。两次返回的 username 字段值不同就说明数据源切换生效了。Mongoose 租户同理返回的是 MongoDB 的 _id 格式文档。提示如果第一次请求某个租户时报连接超时检查对应数据库容器是否已启动以及 .env 里的端口是否和容器映射一致。多租户场景下端口冲突是最常见的坑。6. 本篇常见错排查报错一Nest cant resolve dependencies of UserRepository原因是 UserRepository 没有被注册为 provider或者 UserModule 里漏了某个 Repository 的导入。检查 UserModule 的 providers 数组是否包含 UserTypeOrmRepository、UserPrismaRepository、UserMongooseRepository 和 UserRepository 四个。另外 TypeOrmModule.forFeature 和 MongooseModule.forFeature 都要在 imports 里声明。报错二invalid tenantIdgetTenantPrefix 抛出的错误说明请求头里的 x-tenant-id 不在任何 tenantMap 中。检查三点header 名称是否拼写正确是 x-tenant-id 不是 x-tenanttenantId 是否被 toUpperCase 后仍不匹配对应 ORM 的 constants 文件里是否注册了这个租户。大小写敏感是高频问题Map 的 key 必须全大写。报错三Prisma Client 找不到对应租户的模型多租户下每个租户的 Prisma Client 要单独生成输出目录不同。如果所有租户共用同一个 prisma/client切换租户时模型定义不会变但连接串会串。正确做法是在 schema 里用 generator 指定 output按租户生成到不同目录PrismaConfigService 里按租户前缀 require 对应的 Client。报错四TypeORM 连接数暴涨dataSourceFactory 里的 connections Map 是模块级变量如果服务被多实例部署每个实例各存一份连接数会翻倍。生产环境建议把连接缓存换成外部存储或者限制每个租户的最大连接数。另外记得在应用关闭时遍历 connections 调用 destroy否则热重载会残留连接。报错五AI 工具改代码时提示权限不足settings.json 的 permissions.allow 里没有放行对应命令。多租户项目经常要跑 npx prisma generate 和 npm run start:dev把这两条加进白名单。如果用的是 config.toml检查 tools 段是否禁用了必要能力。7. 接入与排障的下一步这套骨架跑通后多租户数据源切换的核心链路就固定了请求头带 x-tenant-idgetTenantPrefix 解析出租户归属UserRepository 门面按策略选择具体 ORM 实现。后续新增租户只需要在对应 constants 里加一行映射在 .env 里补一组连接信息业务代码不用动。如果你在接入过程中遇到 Key 配置或通道报错可以直接到 API Keys 页面重新生成并核对配置地址是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入细节参考文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。想先验证模型通道是否正常用模型对话页面发一条测试消息最快地址 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果这套多租户架构要长期维护、频繁让 AI 辅助改代码建议直接上 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 额度更稳不会改到一半断流。