在 TypeScript 全栈开发的圈子里,T3 Stack 这几年的热度一直没降过——Next.js 做前端、tRPC 做接口通信、Prisma 管理数据库、Tailwind 搞定样式,外加 NextAuth 处理认证,这套组合把类型安全从数据库一路打通到组件层,用起来确实爽。不过每次新开一个项目,光是初始化、改配置、接路由、配数据库就要折腾大半天,重复劳动严重。我自己的解决思路是写一个名为 t3code 的本地 CLI 工具,用一条命令把整套 T3 Stack 工程模板、认证体系、数据库模型和基础 API 全部生成好,跑完初始化命令就能直接进入业务开发。这篇博文就把 t3code 的设计思路、核心实现和踩过的坑完整记录下来,适合正在用 T3 Stack 做项目、或者想自己搞一套脚手架工具的朋友参考。
1. 项目背景与整体设计:为什么叫 t3code,以及它的核心目标
t3code 这个名字其实就是两个词的组合:t3 代表 T3 Stack,code 代表它最终产出的是可运行的代码工程,而不是简单复制一份静态模板。早期我维护过不少脚手架项目,发现大多数模板仓库的通病是“能跑但改起来麻烦”——模板写死了一些业务假设,开发者拿到手之后要手动删掉不需要的部分,再补上自己的目录结构,这过程并不比从零搭建轻松多少。
t3code 的目标定位很明确:把 T3 Stack 工程的初始化过程变成可交互的命令行操作,按需选择要预装的功能模块,生成与配置完全匹配的工程代码。换句话说,它不是一个代码片段仓库,而是一个能“听懂”你需求、然后替你写代码的生成器。这里说的“听懂”不是自然语言处理,而是通过命令行问答收集你的技术偏好,比如数据库用 SQLite 还是 PostgreSQL,认证要不要集成第三方登录,监控和日志是否需要预置,然后基于这些答案动态渲染模板文件。
1.1 从手动搭建到一行命令的痛点
先说说我为什么非要做这件事。身边不少同事用 create-t3-app 初始化项目,这个官方工具确实好用,但它生成的是一个相对标准的基线工程。实际开发中,每个团队都有自己的规范:有的人喜欢在 src 下按 feature 划分目录,有的人习惯在页面之外单独维护 API 路由;有的项目需要内置 RBAC 权限模型,有的只要简单的邮箱密码登录。这些差异意味着即使你用 create-t3-app 跑完初始化,仍然要花不少时间调整目录、改认证逻辑、建数据库模型。
更头疼的是重复劳动。我每周平均要开两到三个新项目做实验和原型验证,每次都走一遍同样的流程:创建 Next.js 项目、安装 tRPC、配置 Prisma、跑 migrate、改 schema、写一个“hello world”接口测试链路。这套动作熟练之后大概要四十分钟,其中有一大半时间浪费在等待安装依赖和检查版本兼容性上。写成 t3code 之后,同样的操作只需回答五个问题,然后等一到两分钟,就能得到一个结构统一、依赖版本固定的工程目录。
除了速度,t3code 还有一个更重要的价值:一致性。团队协作时,不同成员手动搭建的项目很难保证目录结构完全一致,时间一长就会出现“这个人把 API 放在 pages/api,那个人放在 src/server/api”这种混乱。用同一套生成器初始化项目,至少能保证所有新工程在入口文件、路由组织、数据库客户端封装上遵循同一套约定,后续维护成本会明显降低。
1.2 技术选型:为什么是 T3 Stack 而不是其他全家桶
我之前也试过用 Vite + React + Express 自己搞全栈,后来换了 Next.js,再后来才完整用上 T3 Stack。这套组合最大的优势是端到端类型安全:Prisma 会根据数据库 schema 生成类型,tRPC 的 input 和 output 类型可以在前后端共享,Next.js 的 Server Components 和 Server Actions 又能进一步减少类型边界的磨损。说白了,以前在前后端接口处写手写类型定义的日子,在 T3 Stack 里基本不存在。
t3code 选择 T3 Stack 还有几个现实原因。首先是生态成熟度,Next.js 的 App Router 已经到了非常稳定的状态,tRPC 的适配器对 Next.js 的 React Query 集成也相当完善,Prisma 的 migration 和 client 生成流程清晰稳定。其次是社区资源多,遇到问题搜索一下就有答案,不会像冷门框架那样卡半天没人理。再次是部署方案明确,Vercel 对 Next.js 支持最好,同时 Docker 部署也简单,这一点对需要内网部署的项目非常关键。
当然,T3 Stack 也不是没有争议。很多人嫌 tRPC 只能用于 TypeScript 全栈项目,如果未来要开放 API 给第三方,还得另外写 REST 层。这个顾虑我在设计 t3code 时也考虑过,所以生成的工程里保留了一个极薄的 REST 封装层,用 Next.js Route Handlers 暴露少数必读接口,核心业务仍然走 tRPC。这样既不破坏内部开发体验,也保留了对外覆盖能力。
2. 核心功能拆解与关键参数设计
t3code 的主体功能有三个:模板工程生成、交互式模块选择、初始化后置处理。理解这三个模块,基本就理解了这个工具的架构。
2.1 模板引擎与项目结构生成
模板渲染是整个工具的地基。我最初尝试直接用字符串拼接,把模板文件当作文本,用占位符替换的方式渲染,结果代码里的花括号和 TypeScript 泛型经常冲突,转义转得想摔键盘。后来改用基于简单模板语法的渲染方式:用双大括号包住变量名,配合工具函数进行条件判断和循环渲染,这样既避免了与代码语法的冲突,也保留了模板的可读性。
模板目录是分层设计的,分为基础层和可选层。基础层包含所有 T3 Stack 项目都需要的文件,比如next.config.mjs、tsconfig.json、package.json、根布局文件等。可选层则按功能模块拆分成子目录:feature/auth、feature/database、feature/monitoring、feature/queue等。渲染时根据用户的选择将对应子目录并入输出目录。
项目结构生成有一个关键细节:文件路径中的目录名也需要支持参数化。比如用户输入的项目名是my-admin,生成时所有目录和文件中的app-name变量都要替换成对应的包名和目录名,同时转换成合法的 TypeScript 标识符。这一层处理不好,项目名里带个横线就可能让 import 语句断裂,所以我在生成器里专门写了一个toPascalCase、toCamelCase、toValidImportPath的转换函数组,保证变量在任意场景下都能正确渲染。
2.2 集成 tRPC 与 Prisma 的自动化配置
tRPC 的接入难点不在调用本身,而在构建时的路径别名和数据上下文。生成的工程里,src/server/api/trpc.ts负责创建 tRPC 实例,src/server/api/root.ts负责聚合路由,前后端共享协议类型则放在src/trpc/shared.ts。t3code 在生成时会根据用户选择的数据库类型自动写入DATABASE_URL环境变量模板,SQLite 默认指向本地文件,PostgreSQL 则生成一个连接字符串的占位符,并附上 docker-compose 文件方便本地启动数据库。
Prisma 部分,t3code 默认生成一个最小但完整的 schema。include 一个用户模型、一个会话模型,以及一个用于演示关联查询的示例模型。用户如果不特殊指定,生成器会关闭prisma db push,而是使用 migration 机制,保证生产环境不会因为推送 schema 导致数据丢失。generate命令则放在 postinstall 阶段执行,确保依赖安装完成后 Prisma Client 已经可用。
2.3 认证模块的默认安全基线
认证模块是 t3code 里最容易写崩的部分,因为 NextAuth 的配置涉及很多安全细节。我吸取了之前项目里的教训,在生成代码时直接落地一套安全基线:密码登录使用 bcrypt 哈希加盐,第三方登录只保留 Google 和 GitHub 的示例配置,默认开启 CSRF 保护、Session 策略采用数据库存储,Cookie 设置SameSite=Lax和Secure标记。凡是用户没有明确要求关闭的选项,一律按最保守的方案生成。
Session 回调里会额外校验用户是否处于激活状态,这样即使数据库里的用户被禁用,也无法继续沿用原有 Session。认证相关的 API 路径统一挂在src/app/api/auth/[...nextauth]/route.ts下,组件层的操作封装在src/server/auth.ts中,客户端只通过导出的 hook 调用,避免在页面组件里直接碰 Session 对象。
3. 实操过程:从零实现 t3code 的核心流程
下面梳理 t3code 实现时的完整流程,从初始化项目到最终生成工程,聚焦几个容易出问题的关键节点。
3.1 初始化项目与 CLI 入口设计
t3code 本身是一个 Node.js CLI 项目,入口文件采用commander库解析命令参数。主命令支持t3code init [projectName],后面可以跟--template、--database、--auth、--yes等选项。--yes表示使用默认配置,方便 CI 场景下非交互式调用。交互部分使用prompts库实现,对新手友好,拿回车键和方向键就能选完所有配置。
入口设计最关键的是参数校验。项目名不能包含中文、空格、特殊符号,不能以数字开头,同时需要避开 npm 上的保留名称,这些校验逻辑在validateProjectName函数里全部处理。如果用户输入了不合规的名字,CLI 不是直接报错退出,而是提示一个自动修正后的建议名称,比如把my project自动转成my-project,这样用户体验会好很多。
// src/cli/init.ts import { Command } from 'commander'; import { prompts } from './prompts.js'; import { validateProjectName } from '../utils/naming.js'; export function initCommand() { const program = new Command('init') .argument('<projectName>', '目标项目名称') .option('--template <name>', '模板名称', 'default') .option('--database <type>', 'sqlite | postgres', 'sqlite') .option('--auth <provider>', 'none | credentials | oauth', 'credentials') .option('--yes', '跳过交互确认') .action(async (projectName, options) => { const validName = validateProjectName(projectName); if (!validName) { console.error('项目名称不合法,请使用字母、数字、短横线且不以数字开头。'); process.exit(1); } const config = options.yes ? { template: options.template, database: options.database, auth: options.auth } : await prompts(projectName); await generateProject(projectName, config); }); return program; }3.2 模板渲染与依赖安装的落地细节
模板渲染器是 t3code 的大脑。它接收一个模板路径列表和一个配置对象,输出一个目标目录。渲染流程分三步:扫描模板树、渲染每个文件、写入磁盘。渲染时有一个重要的细节:二进制文件不能走字符串替换,比如项目里如果包含 favicon 或字体文件,需要直接复制。因此文件扫描时按扩展名区分处理,.png、.ico、.woff2走二进制流,.ts、.tsx、.json、.css走文本渲染。
依赖版本管理是另一个关键点。t3code 在生成package.json时不直接写死版本号,而是维护一个dependency-versions.json文件,该文件由 GitHub Actions 每周更新一次,从 npm registry 读取各核心依赖的最新稳定版本。这样生成的工程不会因为某个依赖发布新版本而立刻过时,也避免了直接写latest带来的不可控风险。
依赖安装方面,我选择了默认使用npm install,支持通过--pm选项切换 pnpm。实测下来 pnpm 在同版本 npx 缓存的情况下确实更快,但为了最大兼容性,默认还是 npm。安装过程会输出真实进度并捕获最后的退出码,如果安装失败,t3code 会在当前目录保留生成的工程,但明确提示错误信息,而不是直接全部删除,这样方便用户检查 package.json 中的问题。
3.3 关键文件生成示例与参数计算
下面看一个实际生成的文件,演示 t3code 的核心渲染能力。假设用户输入项目名blog-app,选择 PostgreSQL 数据库和密码登录,生成的.env文件会是这样:
# Postgres 连接串由 t3code 自动组合 DATABASE_URL="postgresql://postgres:postgres@localhost:5432/blog_app?schema=public" NEXTAUTH_SECRET="$(openssl rand -base64 32)" NEXTAUTH_URL="http://localhost:3000"这里的NEXTAUTH_SECRET不是固定值,而是 t3code 在生成时自动调用openssl rand产生的高强度随机数,避免开发者忘记配置而直接使用默认密钥。数据库名则由项目名中的横线转为下划线,避免 PostgreSQL 对连字符的解析问题。
再看 Prisma schema:
// schema.prisma generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) email String @unique passwordHash String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt posts Post[] } model Post { id String @id @default(cuid()) title String content String author User @relation(fields: [authorId], references: [id], onDelete: Cascade) authorId String createdAt DateTime @default(now()) }t3code 生成后会执行prisma migrate dev --name init,把数据库表结构落到本地。这一步是工程初始化的核心体验,搞定了它,后面的 CRUD 接口才能打通。
4. 踩坑实录与常见问题排查
写 t3code 的过程中,我遇到的坑大概能写一篇短篇小说。这里挑最值得分享的几个,按出现的频率排序,每个都附带排查思路。
4.1 模板变量冲突处理的坑
最让我抓狂的问题来自模板语法与 Next.js 内部语法的冲突。比如layout.tsx里有{children},这是 JSX 表达式,但模板引擎也会解析双花括号。我第一版渲染器直接把所有双花括号当成模板变量,结果生成的页面全部丢掉了 children 子组件,而且因为渲染后的 JSX 语法不完整,页面直接白屏。
解决思路是引入占位符转义机制:需要保留的 JSX 表达式在模板中使用{{ "{" }}children{{ "}" }}这种形式,渲染器遇到{{ "{" }}就直接输出{,不会把它当作变量。后来嫌这个写法太累赘,我又给模板引擎加了一个raw块语法,用{% raw %}包裹整段原样输出的代码,只在文件头部和尾部做变量替换。这个改动虽然让模板文件丑了一些,但彻底解决了冲突问题,而且生成出来的代码完全干净。
另一个与变量相关的坑是正则替换的贪婪匹配。早期用replace(/\{\{(.*?)\}\}/g)这样的非贪婪表达式处理嵌套结构,遇到模板里出现三花括号或者 JSX 注释时就会截断错误。后来改成逐字符扫描的解析器,虽然性能稍慢,但正确性提升了很多,尤其处理复杂 JSON 模板时再也不会莫名丢片段。
4.2 版本兼容性问题的处理思路
T3 Stack 的依赖更新很快,React 19 在 Next.js 15 上的支持刚稳定时,我生成工程里还绑定着 React 18 的版本范围,导致新用户安装后收到 peer dependency 警告。后来我专门写了一个dependency-sync脚本,让它每月自动解析 Next.js 的 peerDependencies,并同步更新 t3code 内部的版本映射表。这算是一个省心但需要长期维护的机制,稍微偷懒几天,版本数据就可能过时。
实际的排查思路是:当遇到奇怪的编译报错时,先用npm ls看依赖树中是否存在重复的 React 或 TypeScript 实例,这是 Next.js 工程最常见的疑难杂症。如果某个包对外暴露了类型,但实际引用的是另一个副本的类型,就会出现“类型看似正确却编译不通过”的诡异情况。t3code 生成的工程默认启用strict模式,虽然有新用户抱怨刚开始写代码时错误列表太长,但这能帮助团队尽早发现问题,总比上线前被类型错误背刺要好。
4.3 常见问题速查表
现象:生成工程后 npm run dev 启动失败,提示 PrismaClient is not configured 排查:确认 .env 文件是否存在,DATABASE_URL 是否生效,并执行 npx prisma generate 重新生成 client。 现象:页面中 tRPC 查询一直 pending,刷新后恢复正常 排查:多数因为 React Query 的 queryClient 初始化时机不对,检查 providers.tsx 中 QueryClientProvider 是否在全局根布局挂载。 现象:NextAuth 登录后 session 在浏览器刷新后丢失 排查:检查 NEXTAUTH_SECRET 是否固定,Session strategy 是否为 database,并且确认数据库里生成了正确的 Session 表。 现象:TypeScript 报错找不到 t3code 自动生成的类型 排查:重新运行 postinstall 钩子,或者单独执行 npx prisma generate,确保 .output 目录中的类型文件已经生成。 现象:部署到 Linux 服务器后图片或 Next.js 静态资源 404 排查:检查 next.config 中 output 配置,如果是 standalone 模式,需要复制 .next/standalone 并包含 public 目录。结尾
最后分享一个我实际用起来非常受用的小技巧:t3code 生成工程的同时会写入一个.t3rc.json配置文件,里面记录了生成时的所有选项和依赖版本。后续如果团队想升级统一的依赖版本,或者调整认证策略,不需要重跑脚手架,只需要改这个配置然后执行t3code migrate,工具会自动对比当前工程和配置目标之间的差异,增量更新代码和依赖。这个设计原本是为了我自己的项目维护方便,后来发现放在团队里也很好用,毕竟保持工程初始基线的统一,比大家各自打补丁省心多了。