团队之前为了追求所谓的“微服务化”,把一个好端端 Node.js 大单体强行拆成了五个独立的 Repository。没过三个月,苦果就来了:每个服务都要读写同一张用户表,结果每次修改用户表的 Schema,五个仓库的 Prisma Schema 都得同步手动改一遍。稍微漏掉一个,线上运行就直接崩溃报字段缺失。更别提 JWT 校验、Redis 客户端、日志配置这些基础模块,在每个仓库里都被复制粘贴了一份,改个 Bug 要在五个仓库里提五个 PR。简直是本末倒置!上周我实在忍不了这种心智负担,干脆花了两天时间把这堆烂摊子合并到了一个基于 pnpm workspaces 的 Monorepo 架构里。
重构过程中,最大的痛点在于怎么在多个独立的 NestJS 服务之间优雅地共享 Prisma Client 及其生成的 TypeScript 类型,同时保证各个服务在打包(Build)时不会引入不必要的依赖。
经过几次踩坑,我们最终确立了清晰的架构边界:在根目录下创建 packages 目录,把公共的数据库访问、类型定义和全局配置封装成独立的内部 npm 包,让各业务服务通过 workspace:* 的方式进行本地依赖。
下面是我们最终落地的 pnpm-workspace.yaml 配置,以及在公共数据库包 packages/database 中,通过自定义生成路径(output)让所有子服务共享同一份 Prisma Schema 和 Client 的关键配置:
# pnpm-workspace.yaml
packages:
- 'apps/*'
- 'packages/*'
公共数据库包 packages/database/package.json 的具体声明如下:
{
"name": "@mono/database",
"version": "1.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"db:generate": "prisma generate",
"db:migrate": "prisma migrate dev",
"build": "tsc"
},
"dependencies": {
"@prisma/client": "^5.12.1"
},
"devDependencies": {
"prisma": "^5.12.1",
"typescript": "^5.4.5"
}
}
而我们在 packages/database/prisma/schema.prisma 中,特别通过指定生成路径,避免了 Prisma 默认生成到 node_modules 导致多包环境下类型找不到的玄学问题:
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
output = "../src/generated/client"
}
model User {
id String @id @default(uuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Product {
id String @id @default(uuid())
title String
price Float
stock Int
createdAt DateTime @default(now())
}
在 packages/database/src/index.ts 中,我们直接导出生成的 Client:
import { PrismaClient } from './generated/client';
export * from './generated/client';
export const db = new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
而在具体业务服务(比如 apps/user-service)中,我们只需要在 package.json 里添加依赖:
{
"name": "user-service",
"dependencies": {
"@mono/database": "workspace:*"
}
}
通过这种方式,任何服务如果需要操作数据库,直接从 @mono/database 导入已经实例化好的 db 或者相关的 Prisma 类型。当我们需要修改表结构时,只需要在 packages/database 下运行 pnpm db:migrate,所有引用了该包的业务服务在编译时就会自动获取最新的 TypeScript 类型提示,再也不会出现线上环境与数据库 Schema 不一致导致的低级 Bug。
另外,针对不同微服务可能需要不同环境变量的问题,我们在公共配置包 @mono/config 里利用 zod 做了强类型的校验。凡是缺少必要环境变量的服务,在启动阶段就会直接报错抛出,决不允许带着未定义的 Undefined 裸奔上线。
目前这套 Monorepo 跑下来,开发体验提升了不止一个档次。唯一比较纠结的是,随着业务越来越复杂,Prisma Schema 的行数已经奔着上千行去了,单文件维护开始变得臃肿。大家在 Monorepo 里一般是怎么拆分 Prisma Schema 的?是用第三方工具拼接,还是干脆上多 schema 模式?