前端改了个类型,后端上线才炸:vibe 项目的 Monorepo 实战
三个信号说明你该 monorepo 了:一个功能要开 3 个 PR、类型对不上线才炸、AI agent 老改漏。这篇给你可直接抄的目录骨架、Turborepo 编排、类型共享实战,以及 Vercel 与 Docker 各自的部署坑。

先划清边界:这篇不讲什么
仓库和流程的话题站内有三篇,先说明分工:
- 《一人 AI 的 Git 工作流》讲的是「分支怎么玩」——main 直推还是 PR、AI 生成 commit 信息怎么审;
- 《vibe 项目的开发环境标准化》讲的是「本地环境怎么统一」——devcontainer;
- 《vibe 项目的 CI/CD 最小管线》讲的是「代码怎么自动上线」——5 个 Job。
本篇只回答一个问题:「代码放几个仓库、包之间怎么引用。」前端、后端、共享组件、数据库 schema——放在一个仓库里用 workspace 管起来,还是一人一个仓库?这是决定你未来一年开发体感的结构性选择。
三个信号:你该 monorepo 了
别为了「大厂也这么干」而上 monorepo。对照这三个信号,中了两条再动手:
- 跨仓库改同一个功能要开 3 个 PR。前端加个字段,后端、共享类型、文档各一个 PR,合并顺序还不能错——协调成本已经超过 monorepo 的学习成本;
- 「类型对不上」是线上 bug 的常见死因。后端改了 API 返回,前端还是旧类型,TypeScript 在各自仓库里都「编译通过」,上线才炸;
- AI agent 经常改漏。让 AI「把用户名字段改成可选」,它改了前端,忘了后端校验和数据库迁移——因为它一次只能看一个仓库。
| 多仓库(polyrepo) | 单体 monorepo | |
|---|---|---|
| 适合 | 完全独立的产品线、需要独立权限/审计 | 一人团队的前后端 + 共享包 |
| 跨包改动的原子性 | 无:N 个 PR + 合并顺序 | 有:一次提交全改完 |
| 类型一致性 | 靠发版同步,滞后 | tsc 一次全查,错了当场报 |
| CI 复杂度 | 每个仓库一套 | 一套,但要配 filter(见下文) |
| 心智负担 | 低(每个仓库简单) | 中(要理解 workspace 引用) |
目录结构:一份能直接用的骨架
myapp/
├── apps/
│ ├── web/ # Next.js 前端
│ └── api/ # NestJS/FastAPI 后端
├── packages/
│ ├── ui/ # 共享 React 组件
│ ├── db/ # Prisma schema + 迁移 + client
│ ├── schemas/ # zod 校验 schema(前后端共用)
│ └── config/ # tsconfig / eslint 共享配置
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
包之间用 workspace 协议互相引用,前端 package.json 里写 "@myapp/ui": "workspace:*",pnpm 会创建 symlink——本地改 packages/ui,apps/web 立刻生效,不用发版、不用 npm link 的玄学。

Turborepo:让构建按依赖拓扑跑
monorepo 的构建不能 cd apps/web && npm run build 了事——packages/db 改了,api 和 web 都要重编。Turborepo 干的就是这个编排:
// turbo.json
{
"$schema": "https://turborepo.dev/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"], // 先构建依赖包,再构建自己
"outputs": ["dist/**", ".next/**"]
},
"db:migrate": {
"dependsOn": ["^db:generate"],
"cache": false // 迁移永远真实执行,不缓存
},
"dev": { "cache": false, "persistent": true },
"lint": { "dependsOn": ["^build"] }
}
}
^build 的意思是「先跑我依赖包的 build」。你在根目录执行 turbo run build,它会按拓扑排序:config → db/schemas/ui → api/web,自动并行无依赖的任务。更重要的是缓存:没改过的包直接命中缓存,turbo run build 第二次跑只要几秒——AI agent 反复改代码时,这个缓存能省大量等待时间。
远程缓存(Vercel Remote Cache)一人团队免费额度够用,CI 里配上,构建时间再砍一半。
类型共享实战:让「对不上」在编译期就炸
这是 monorepo 对 vibe 项目最大的价值。三个共享点:
1. 数据库 schema 即类型源。packages/db 放 schema.prisma,prisma generate 产出的 client 被 api 和 web 同时引用。改字段类型:
// packages/db/package.json
{ "name": "@myapp/db", "exports": { ".": "./client.ts" } }
// packages/db/client.ts
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
export type { User, Post } from '@prisma/client';
2. API 契约前后端共用。packages/schemas 放 zod schema,后端用它校验入参,前端用它推导表单类型:
// packages/schemas/src/post.ts
import { z } from 'zod';
export const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
});
export type CreatePost = z.infer<typeof CreatePostSchema>;
// 后端:app.post('/posts', zValidator(CreatePostSchema))
// 前端:const data: CreatePost = form.getValues() // 类型自动对齐
3. 根目录一次 tsc 全查。turbo run typecheck 跑所有包的 tsc --noEmit。后端改了返回字段但忘了改 schema,前端的类型错误会和后端的一起报出来——在提交前,不是在线上。
部署:两个平台各自的坑
Vercel(前端 apps/web):项目设置里 Root Directory 填 apps/web,Build Command 用 cd ../.. && turbo run build --filter=web...(...` 表示带上依赖包)。环境变量里加 TURBO_REMOTE_CACHE_SIGNATURE_KEY 开远程缓存。注意:Vercel 的 monorepo 支持只认 pnpm/npm/yarn workspace,packageManager 字段写死版本。
Docker(后端 apps/api):这是 monorepo 最大的坑——Docker 构建上下文。Dockerfile 必须放仓库根目录(context 是整个仓库),然后精确 COPY:
# 仓库根目录的 Dockerfile.api
FROM node:20-slim AS build
WORKDIR /app
# 只拷 api 及其依赖包,保持层缓存有效
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json turbo.json ./
COPY apps/api/package.json ./apps/api/
COPY packages/db/package.json ./packages/db/
COPY packages/schemas/package.json ./packages/schemas/
RUN npm i -g pnpm && pnpm install --frozen-lockfile \
--filter @myapp/api... # 只装 api 及其依赖
COPY apps/api ./apps/api
COPY packages/db ./packages/db
COPY packages/schemas ./packages/schemas
RUN pnpm --filter @myapp/api build
FROM gcr.io/distroless/nodejs20-debian12
WORKDIR /app
COPY --from=build /app/apps/api/dist ./dist
COPY --from=build /app/node_modules ./node_modules
USER nonroot
ENTRYPOINT ["node", "dist/index.js"]
关键在 --filter @myapp/api...:pnpm 只安装 api 及其依赖包,不把 web 的 Next.js 也装进来。COPY 顺序同样遵循层缓存铁律:package.json 先,源码后(呼应本轮 g1 的 Dockerfile 指南)。
CI:只构建受影响的包
monorepo 的 CI 如果每次全量构建,改一行文案也要等 10 分钟。Turborepo 的 filter 解决:
# .github/workflows/ci.yml 片段
- run: pnpm turbo run build lint typecheck --filter=...[HEAD^]
# ...[HEAD^] = 本次提交影响到的包及其依赖/被依赖者
只改了 apps/web 的样式?CI 只跑 web 的 build+lint。改了 packages/db 的 schema?api 和 web 的 typecheck 自动被带上——影响范围由依赖图决定,不用人脑记。这正是 monorepo 在 CI 上的复利:改得越多,省得越多…反过来,改得越少,跑得越快。
版本发布:一人团队的 changesets 轻量用法
packages 将来要发 npm 包?先别上 changesets 全套。一人团队够用方案:所有包 version: 0.0.0 占位,workspace 协议引用不依赖版本号;真要对外发布那天,再 pnpm changeset init。过早引入版本管理流程,是一个人能犯的最贵的「流程税」。
4 个坑:提前知道就不叫坑
- 幽灵依赖(phantom dependency)。pnpm 默认严格隔离,a 包没声明却用了 b 包的代码——在 npm 下能跑,在 pnpm 下炸。修法:缺啥在用到的包里显式声明,别靠 hoisting 的运气。
- IDE 跳转进 .d.ts 而不是源码。packages 的
exports指向 dist,Ctrl+点击跳到编译产物。修法:开发期用publishConfig+exports条件导出,或配 tsconfig paths 指向src。 - AI agent 在 monorepo 里迷路。仓库一大,agent 找文件靠猜。修法:在根目录
AGENTS.md里写「包地图」:每个包一句话说明 + 什么时候该改它。这是本轮《AGENTS.md 项目手册》指南的直接应用。 - 把 secrets 放进共享包。packages/config 里放了带 key 的配置文件,所有 app 都能读到。修法:配置包只放结构,secret 永远走环境变量(呼应 g1 的 secret 管理)。
上线前 checklist
- ☐
pnpm install在干净环境一次通过,lockfile 已提交 - ☐ 根目录
turbo run build全绿,第二次跑命中缓存(几秒内) - ☐
turbo run typecheck覆盖所有包,无错误 - ☐ 改 packages/db 字段后,api 和 web 的 typecheck 都报错(验证联动有效)
- ☐ Vercel Root Directory / build filter 已配,预览部署成功
- ☐ Dockerfile 在仓库根目录,
--filter只装需要的包 - ☐ CI 用
...[HEAD^],只改 web 时 CI 不跑 api 的构建 - ☐ AGENTS.md 里有包地图,AI agent 能找对地方
总结一句话:monorepo 不是「把代码放一起」,是把「跨包一致性」从人的记忆力外包给工具链——tsc、turbo 的依赖图、workspace 引用。对一人一 AI 的团队,这是用结构换脑力的划算买卖。
评论 (0)
相关文章

AI 生成的 Dockerfile 有 5 个常见毛病:单阶段构建、root 运行、无健康检查、收不到 SIGTERM、secret 打进镜像层。这篇给你多阶段构建模板、基础镜像选型决策表、生产级 compose 编排和上线前 checklist。

账单发到客户手里却变成乱码?本篇教你用 Puppeteer Page.pdf 做中文排版、pdf-lib 做合并盖章,覆盖字体子集化、表格跨页、页眉页码、大文件分片生成与沙箱安全,一次搞定 vibe 项目的 PDF 成品交付。

用户写的 2000 字,要扛住浏览器崩溃、断网和误关标签页。三层防线——输入防抖、localStorage 兜底、带版本号的服务端持久化,外加版本号乐观锁的冲突 UX(保留两份让用户选)、离线重放与草稿生命周期。