资讯动态

t3code 实战:全栈 TypeScript 工程化脚手架从入门到生产落地

发布时间:2026/10/28 4:31:15 来源:尧图企业网站定制
t3code 这个名字第一次出现在我面前的时候我愣了半天没看出来它是个什么来头。后来翻了翻它的文档和模板才反应过来这不就是把当下全栈 TypeScript 生态里最好使的那些东西用一套工程化流程串起来了吗。这段时间我一直在折腾 t3code也用它在真实项目里跑了好几个全栈应用从初始化、建表、写接口到部署上去整个流程算是摸透了。这篇就来聊点实在的t3code 到底是什么思路、核心细节怎么落地、实际跑起来会遇到哪些绕不开的坑以及我从这些经验里总结出来的一套可复用的操作方法。1. t3code 的定位它解决的不是“写代码”而是“起手式”1.1 全栈 TypeScript 项目的“起手式”到底有多重要做 Web 开发的人应该都有过这种经历新项目开工第一天最高效的事往往是复制别的项目的目录结构把 package.json、tsconfig.json、tailwind.config.js 全都搬过来再手动删掉无关代码。这种做法不是不行但它带来的问题是每个项目的结构都会逐渐长歪有人喜欢把接口写在pages/api下面有人习惯单独建server/router目录环境变量的管理方式千奇百怪ORM 配置更是五花八门。t3code 想做的事情说白了就是把这些“起手式”固定成模板和脚手架。它不是编程框架不去替代 React、Next.js 或者 Express而是一个工程化工具帮你在 30 秒内得到一个目录规范、依赖版本统一、开箱即用的全栈项目。用生活里的话说它不是替你盖房子而是把毛坯房的水电图纸、墙体结构、管线槽位都提前画好你进去只需要做软装。t3code 这个名字里的 3指的不是三个插件而是它对 TypeScript 生态里那条“类型安全”主线的坚持。从后端数据模型到 API 层再到前端页面全程走 TypeScript 类型推导不让数据在边界上变成any这是 t3code 和普通代码生成器最大的区别。1.2 三类核心能力init、add、generate如果你用过其他脚手架比如老牌的 CRA 或者后来的 Vite CLI会发现它们更多的是“初始化项目”这一件事。t3code 的设计要更细一点它把日常开发中的高频动作拆成了三类。第一个是t3code init用来初始化一个完整项目交互式选择技术栈、数据库、认证方案然后生成工程。第二是t3code add这个灵感来自某些组件库的按需添加模式项目跑起来之后想加一个日志模块、加一个文件上传模块、加一套错误监控上报代码不需要自己翻文档去集成直接执行 add 命令t3code 会把对应的模板文件和依赖装好。第三是t3code generate它面向的是重复性的 CRUD 代码生成你定义好数据模型自动生成数据库迁移脚本、tRPC Router 的增删改查接口和前端表单页面骨架。这三件事对应到开发流程中分别解决的是“项目从哪来”“新能力怎么加”和“重复代码怎么写”的问题。一个团队如果统一用 t3code 作为入口那么新人接手项目时的认知成本会显著降低因为所有项目长一个样配置入口都在固定的位置。1.3 为什么不用现成的 create-t3-app而要自己做 t3code很多人会问已经有 create-t3-app 了为什么还要搞一个自己的工具。我当时的理由是create-t3-app 更像是给“新玩家”快速体验 T3 技术栈的一个入口它生成的模板偏演示性质。而 t3code 在团队实践里需要解决更具体的问题比如内部统一的服务端部署配置、私有 npm 包的接入、统一的 ESLint 规则集还有按团队规范定制的目录结构。这些诉求没法靠一个通用模板解决必须有一套可扩展的模板机制。所以 t3code 在设计时把模板做成了独立模块团队可以维护自己的模板仓库t3code 只负责拉取、渲染、安装依赖和初始化 Git。这样它就从“工具”变成了“团队工程规范的载体”。你完全可以理解为create-t3-app 是给你体验的样板间t3code 是你可以带着自己的设计图纸进去施工的装修队。2. t3code 的关键设计与实现细节藏在命令背后的那套逻辑2.1 命令行交互与参数解析为什么选 commander 加 promptst3code 本质上是一个命令行工具所以它的技术底座是 Node.js。在参数解析这一层我试过直接用process.argv手写解析但很快就放弃了因为要做子命令、全局选项、参数校验手写容易漏边界情况。最终采用的方式是commander负责子命令结构和选项解析prompts负责交互式的选择列表、输入框和确认项。这两者的分工很明确commander 只关心“用户输入了什么”prompts 只关心“用户想要什么”。比如执行t3code init my-app --db prisma --auth next-authcommander 会把my-app解析成项目名把--db和--auth解析成选项。但如果用户没有传--db流程就进入 prompts 的交互环节在终端里列出一个表格让你用上下键选择 Prisma 还是 Drizzle选择完成后提示要不要顺便初始化 Git。这里有一个细节值得说下交互式选择的结果必须缓存到临时配置对象里而不是直接驱动模板渲染。因为用户可能在选择过程中反悔先选了 Prisma后来又改成 Drizzle如果前面的选择直接改写了文件后面就得做回滚。t3code 的做法是先收集完所有答案统一写入内存中的 context 对象确认无误后才执行文件写入。2.2 模板引擎的选择ejs 为什么够用但有几个坑必须避开生成项目文件的时候t3code 需要把模板里的变量替换成用户的实际输入。模板引擎我选的是 ejs而不是更复杂的 handlebars原因很简单ejs 就是在 HTML/TS 文件里嵌入% variable %学习成本几乎为零而且支持原生的 JavaScript 表达式写条件渲染的时候特别直接。但 ejs 有个坑如果你的模板文件里本身就有%或者%这种字符串ejs 会尝试去解析它并报错。最典型的就是生成一个包含 CSS 内容或者包含模板字符串的 TS 文件里面写了个%结果渲染阶段直接崩掉。规避方案是在模板里使用 ejs 的转义写法%%它会原样输出成%。另一个更省事的方案是对于完全没有动态变量的静态文件不要让 ejs 去渲染直接二进制复制原文件这样既避免了解析冲突也提升了生成速度。t3code 的模板目录里区分了inline/和static/两类文件前者走 ejs 渲染后者走文件复制。2.3 依赖版本锁定为什么 t3code 里坚持用 pnpm 加 lockfile工程类工具最容易翻车的地方就是依赖版本。t3code 生成项目时会在 package.json 里写一批依赖比如 Next.js、React、Prisma、tRPC 等。如果这些版本号写成^15.0.0这种范围版本那么不同时间初始化出来的项目实际安装的依赖可能差了几个小版本。几个月后团队里两个新项目的 Next.js 版本对不上行为表现也会有细微差异排查起来非常费劲。所以 t3code 的所有内置模板都锁定精确版本并且同时提交 pnpm-lock.yaml。为什么是 pnpm 而不是 npm 或 yarn一方面是 pnpm 的依赖安装速度确实快磁盘占用也少另一方面是 pnpm 对 monorepo 和 workspace 的支持非常成熟t3code 后续计划支持多包项目pnpm 是符合长期方向的选择。这里要提醒一句生成项目之后包管理器不要随意切换。npm 不会读取 pnpm 的 lockfileyarn 也不会切换之后会重新解析依赖可能会产生不一致的依赖树。t3code 在init的最后一步会检查当前目录下的锁文件类型如果发现已有 package-lock.json 会让用户确认是否切换到 pnpm。2.4 配置合并机制模板默认值、用户输入和本地覆盖三层优先级t3code 的配置管理遵循一个简单的优先级模型本地项目配置覆盖用户输入用户输入覆盖模板默认值。比如模板自带的next.config.js里默认开了reactStrictMode: true用户在执行 init 时没有传任何相关选项那么生成出来就是开启的。如果用户在交互式界面里专门关掉了 StrictMode那就会在 context 里打上标记模板渲染时通过% onlyIf(reactStrictMode) %这种方式决定是否输出相关配置。但真正的难点是“本地项目配置”这一层。t3code 项目里通常会有一个t3code.config.ts文件它会定义这个项目要用的团队规范比如路由风格、测试框架、代码风格等。生成之后如果用户改了配置并且重新执行t3code generate那么 generate 阶段必须以本地配置为准而不是再问一遍用户。为了实现这个逻辑t3code 在解析配置时做了一个分层加载先读默认配置再读命令行参数最后读本地项目配置后者的值会覆盖前者。这套机制说起来简单实现起来最麻烦的是“数组类型的合并”。比如默认的 ESLint 插件列表和用户自定义的插件列表是替换关系还是合并关系t3code 的约定是数组默认替换但支持$push标记表示追加。这个语法初看不直观但用习惯了就很顺手。3. 实操过程用 t3code 从零起一个带数据库的全栈应用3.1 安装与初始化一条命令跑通项目脚手架先用一个干净目录做演示。安装 t3code 本身很简单我用的是 npm 全局安装命令是npm install -g t3code装完之后执行t3code --help能看到 init、add、generate 三个子命令的说明。接下来初始化一个项目名叫 blog-app 的应用t3code init blog-app --stack full --db prisma --auth next-auth这里--stack full表示生成完整的前后端一体应用--db prisma表示使用 Prisma 作为 ORM--auth next-auth表示接入 NextAuth 认证。命令执行后交互式流程会追问几个问题比如 UI 组件库选哪个、是否需要示例页面、是否初始化 Git。全部确认后t3code 会创建 blog-app 目录开始拉取模板安装依赖。安装完成后进入项目看一下目录结构cd blog-app find . -maxdepth 2 -type d | sort你会发现一个典型的 T3 风格结构src/pages放页面路由src/server/api/routers放 tRPC Routerprisma/schema.prisma是数据模型定义src/styles是全局样式目录。这个结构不是 t3code 独有的但它把所有东西都固定在了约定俗成的位置团队协作时不需要争论“接口文件放哪”这种哲学问题。3.2 配置环境变量这一步最容易翻车初始化完成后第一件事不是写代码而是配置环境变量。t3code 生成的.env.example文件里列出了所有需要的变量最核心的有三个DATABASE_URL、NEXTAUTH_SECRET、NEXTAUTH_URL。我直接复制一份为.env然后填上实际值DATABASE_URLpostgresql://postgres:postgreslocalhost:5432/blog-app NEXTAUTH_SECRETlocal-dev-secret-change-me NEXTAUTH_URLhttp://localhost:3000如果你是拿 Docker 跑 PostgreSQL可以先用下面这串命令拉一个临时数据库实例docker run --name t3code-postgres -e POSTGRES_USERpostgres -e POSTGRES_PASSWORDpostgres -e POSTGRES_DBblog-app -p 5432:5432 -d postgres:16这里有个小细节NEXTAUTH_URL 在本地开发时是http://localhost:3000但部署到线上之后必须改成线上正式域名否则认证流程的跳转会失败。很多项目上线后报错排查半天才发现是这个变量没更新。3.3 定义数据模型并完成首次迁移Prisma 在 t3code 里的正确用法接下来打开prisma/schema.prisma添加一个最基础的文章模型model Post { id String id default(cuid()) title String content String published Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt }保存之后执行数据库迁移npx prisma migrate dev --name init-post这条命令做了两件事生成了 SQL 迁移文件并且把它应用到本地数据库。执行完之后通过npx prisma studio可以打开图形化管理界面直接看到 post 表已经创建好了。在这个阶段最容易遇到的问题就是DATABASE_URL配错导致连接超时。t3code 模板里的 Prisma 客户端默认设置了?connection_limit5pool_timeout10这样的参数本地用没问题但生产环境下要看数据库服务商的连接数限制否则会有连接池耗尽的风险。后面会详细说。3.4 写一个 tRPC 接口从数据库到前端页面的完整链路数据模型建好了现在写一个查询接口把文章列表从数据库取出来挂到 tRPC Router 上然后在页面里调用。tRPC 的 Router 文件在src/server/api/routers/post.ts我加了一个查询方法import { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; export const postRouter createTRPCRouter({ listAll: publicProcedure.query(async ({ ctx }) { return ctx.db.post.findMany({ orderBy: { createdAt: desc }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1), content: z.string().min(1), })) .mutation(async ({ ctx, input }) { return ctx.db.post.create({ data: { title: input.title, content: input.content }, }); }), });注意这里的ctx.db它是 t3code 模板里预先初始化好的 PrismaClient 实例挂在了 tRPC 的 context 上。这个设计避免了每次请求都重新创建数据库连接也保证了类型可以从 Prisma Schema 自动生成到 Router 层。然后注册 Router在src/server/api/root.ts里加上post: postRouter。接下来是前端页面在src/pages/index.tsx里做一个列表页import { api } from ~/utils/api; export default function Home() { const { data: posts, refetch } api.post.listAll.useQuery(); return ( main classNamemx-auto max-w-3xl p-8 h1 classNametext-2xl font-bold最新文章/h1 ul {posts?.map((post) ( li key{post.id} classNameborder-b py-4 div classNamefont-semibold{post.title}/div p classNametext-gray-500 text-sm{post.content}/p /li ))} /ul /main ); }跑一下npm run dev打开http://localhost:3000如果能看到文章列表说明整条链路已经打通页面通过 tRPC 客户端发起请求Router 调用 Prisma 查数据库数据以类型安全的方式返回前端全程没有手写一个 REST API也没有手写一个fetch调用。3.5 用 generate 命令快速添加完整 CRUD 模块如果每个模块都像上面那样手写 Router、手写页面、手写表单还是会有些重复。t3code 的generate命令就是为这个场景准备的。比如我想给文章模块加一个管理后台的 CRUD 页面执行t3code generate crud --model Post --fields title:string content:string published:boolean它会读取prisma/schema.prisma里的 Post 模型根据字段类型生成对应的 TS 类型、zod 校验、tRPC Router 方法和管理页面。生成的代码并不一定完全符合业务需求但它给出了一个可运行的基础版本你只需要在生成产物上调整细节而不是从空白文件开始敲。这里我要多说一句代码生成工具最忌讳的是生成“一次性死代码”也就是生成完就没人维护的东西。t3code 的做法是把生成逻辑做成可重复执行的如果你改了 Post 模型重新执行 generate会在旧文件基础上做增量更新而不是一删了之。这个设计能保证业务演进时生成代码和数据库模型始终是同步的。4. 生产环境实战我踩过的坑和排查思路4.1 环境变量丢失本地能跑线上就崩第一优先查这个用 t3code 生成的项目里所有数据库地址、密钥、回调地址都来自环境变量。如果本地跑得好好的部署到服务器上却提示数据库连接失败或者认证报错不用怀疑直接去看环境变量有没有完整同步。我自己的经验是先去.env.example对一遍看线上配置是否覆盖了所有变量。其次是注意变量名的大小写t3code 模板里统一使用DATABASE_URL和NEXTAUTH_SECRET有人手动配置时写成databaseUrl或者NextAuthSecret程序读不到报错还非常隐晦。另外要注意的是生产环境尽量用密钥管理服务不要把真实密钥直接写进构建流程的配置文件里。即使只是个人项目也建议至少把.env文件加入.gitignore防止密钥被提交到代码仓库中。提示如果用的是 docker 部署务必通过 docker compose 的 env_file 或环境变量字段注入不要把.env文件直接复制进镜像里那样会让密钥跟着镜像走后续轮换时要重新打镜像。4.2 Prisma 在 Serverless 环境下的连接问题t3code 的模板默认给 Prisma 加了连接池参数datasource db { provider postgresql url env(DATABASE_URL) }但在 Serverless 环境下函数实例会被频繁创建和销毁每个实例都会尝试建立数据库连接如果不做限制很容易把数据库连接数打满。真遇到这个情况数据库侧的报错通常是remaining connection slots are reserved for non-replication superuser connections。解决思路有两个。一个方案是把 Prisma 客户端提升到模块作用域用全局单例模式维护避免每个请求都重新实例化import { PrismaClient } from prisma/client; const globalForPrisma globalThis as unknown as { prisma?: PrismaClient }; export const db globalForPrisma.prisma ?? new PrismaClient({ log: [error] }); if (process.env.NODE_ENV ! production) globalForPrisma.prisma db;另一个方案是在DATABASE_URL里控制连接池大小比如加上connection_limit1这样的参数让每个函数实例尽量少占用数据库连接。这两者最好同时做一个是应用层面的优化一个是连接层面限流。4.3 Next.js 水合不一致页面预览和渲染结果不一样用 t3code 生成项目后如果你在页面里用了new Date()或者Math.random()这种不稳定的值生产环境跑起来会报 Hydration 错误。它的现象很经典浏览器控制台提示文本内容不匹配页面刷新后模块时好时坏。这是因为 Next.js 服务端渲染时先生成了 HTML 字符串客户端加载后又执行了一遍 React要求两边结果一致。如果服务端生成的日期和客户端生成的日期不一致React 就会报错。t3code 模板里默认开启了reactStrictMode所以这个错误暴露得会更加直接。处理方式不是什么高深的技巧就是把这些不稳定值放到useEffect里再更新或者用suppressHydrationWarning属性标识出特定元素。我习惯的做法是后者用得越少越好前者才是正道。4.4 tRPC 请求跨域本地代理和生产路径配置t3code 生成的前后端一体应用通常不需要显式配置 CORS因为页面和 API 在同一个 Next.js 服务里。但如果你把 API 单独拆出去或者前端部署在别的域名下跨域问题就来了。t3code 模板中tRPC 的 HTTP 服务挂在 Next.js 的 API 路由下面路径是/api/trpc。如果前端页面和后端服务不在同一域名必须显式配置cors()中间件把前端的域名加入允许列表。另外一个容易忽略的点是部署环境中的路径前缀。如果应用部署在子路径下比如https://example.com/blog那么所有资源路径和 API 请求路径都要加上/blog前缀否则页面会请求https://example.com/api/trpc而不是https://example.com/blog/api/trpc导致接口 404。4.5 数据库迁移在生产环境的“先改后加”陷阱用 t3code 开发项目时prisma migrate dev可以在本地随意重置和重新迁移。但生产环境是不同的数据库里已经有真实数据了。很多人在生产环境执行prisma migrate deploy时发现某个字段的变更会导致数据丢失甚至迁移失败。最典型的坑是给已有的表添加了一个required类型的字段生产环境上这张表里已经有旧数据旧行的新字段是空的数据库不允许为空迁移直接报错。正确做法是分两步走先添加一个允许为空的字段写个脚本把旧数据填充完整然后再把字段约束改成必填。t3code 的模板里附带了一份数据库变更规范文档核心原则只有一条生产环境的迁移必须考虑存量数据不能只看新项目里的空表状态。5. 团队落地 t3code 的三点建议从个人项目到协作规范化5.1 模板定制不应该靠改生成产物而是改模板仓库我见过不少团队用脚手架生成项目后手写了大量补丁代码来满足业务规范最后每个项目都长得不一样。用 t3code 的正确姿势是把公共规范沉淀到模板仓库里包括 ESLint 规则集、目录结构、日志格式、错误上报插件、CI 工作流模板等。t3code 的模板仓库默认是公开提供的但它在设计上支持通过配置文件覆盖模板源。你可以把模板仓库改成自己团队维护的私有仓库然后所有人统一用同一个命令入口初始化项目。这样生成的项目天然具备团队规范不需要每次靠口头约定。这里我要强调的是模板仓库和业务代码仓库要分开。模板仓库追求“稳定”业务代码仓库追求“变化”。模板仓库的改动要走发布审批流程业务项目则通过升级依赖的方式接收模板变更而不是直接改动生成产物。5.2 add 命令让“技术选型”变成“业务决定”传统项目里引入一个新的技术模块往往要经历漫长的技术评审而且不同项目的实践方式还容易不一致。t3code 的add命令把这个过程简化成了几步执行命令、选择模块、等待安装完成。这样做的好处还在于模块的选择记录是可追溯的。哪个项目用了哪个模块、什么版本、配置如何都可以通过项目里的配置文件查出来。加上这个模块在团队内已经经过预检踩过的坑都写进了模块说明里后续维护成本大幅降低。5.3 代码生成器要克制别指望它解决所有问题t3code 提供了 CRUD 生成能力但我其实建议你在使用的时候保持克制只给重复性高、结构稳定的场景使用。比如管理后台的列表、基础表单、数据字典维护这些模块适合生成。但核心业务逻辑、复杂的权限判断、跨模块的状态同步还是手工编写更合适。代码生成器有个天然局限它只能处理“模式已知”的情况而复杂业务恰恰就是模式未知、需要不断探索。把生成器用在架构稳定的外围模块把核心逻辑留给人来设计这才是工程化工具的正确使用方式。6. 关于 t3code我最后想说的我实际跑完一整套流程之后最大的体会是类似 t3code 这样的工程化脚手架真正节省的并不是敲代码的时间而是团队心智对齐的时间。新成员入门不用再翻十几篇文档搞懂项目结构做成一个功能模块也不需要去问前辈“接口应该写在哪个文件”。这种规范化的收益比较隐性但长期积累下来非常可观。如果你正准备从零做一个全栈项目或者你想把团队的工程规范沉淀成一套可复用的工具我建议你先拿 t3code 生成一个最小项目跑通一条从数据库到页面的完整链路再根据自己的业务去调整模板。工具本身不复杂真正复杂的是你希望它承载的那套团队约定。与其让每个项目都长成不同的样子不如一开始就固定好起手式把精力留给真正需要创造力的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑