1. 项目概述:t3code 是什么
把t3code拆开来看,t3指的是社区里已经很成熟的 T3 技术栈(Next.js、TypeScript、Tailwind、tRPC,再加上 Prisma 和 NextAuth 这两根常用支柱),code才是重点——它既是一套工程规范,也是一台帮你把业务代码“生成”出来的工具。我自己的理解就是:这是一套以 T3 技术栈为核心的全栈项目脚手架,同时随附一套能按模块自动生成 CRUD 代码、页面、组件和路由封装的生产力工具。
这套东西解决什么场景?很简单:开新项目时,最怕的不是写业务逻辑,而是花两个多小时在项目根目录里反复配置next.config、tailwind.config、tsconfig.json、Prisma 初始化、NextAuth 接入、tRPC 路由挂载。哪怕你已经熟练到背得出大部分配置项,每次重复仍然是对耐心的消耗。t3code把这部分前置工作收拢成一条npx t3code@latest new my-app命令,交互式选择技术偏好后,就能得到一个结构清晰、类型链路完整、能直接pnpm dev跑起来的项目骨架。
更进阶的价值在于“模块生成器”。工程初始化只是把篮子编好,真正的内容还得靠业务模块往里填。t3code允许你用generate子命令一次生成一个完整业务模块,比如博客、商品、账单、工单,生成范围包括 Prisma 数据模型、tRPC Router、Zod 输入校验、列表页、详情页、表单组件和对应客户端 hooks。生成的代码风格统一、命名一致,还能自动完成 router 注册这类最容易遗漏的接线工作。所以它比普通的 GitHub 模板更聪明,相当于从“下模板”升级到了“按需生成”。
这篇内容主要写给三类人:准备用 Next.js 做全栈项目的新手,想把团队多项目基线统一起来的工程化负责人,以及正在学 T3 Stack 并希望理解“类型安全如何穿透全链路”的进阶者。我会把技术选型的逻辑、初始化实操、模块生成器的用法、常见故障排查全部过一遍,尽量把我踩过的坑和总结出来的习惯也一并写清楚。看完之后,你至少能在一小时内把一个带数据库、带认证、带若干个可操作业务模块的应用跑起来。
1.1 项目背景与目标
T3 技术栈在开发者社区里流行,核心不是因为它用了多前沿的技术,而是它非常“克制”。它不强制你用服务端渲染、不逼你上 monorepo、也不要求你在微服务和单体之间做复杂的架构抉择。它希望用最少的概念完成“前端页面 + 后端逻辑 + 数据库读写”的闭环,并且让 TypeScript 的类型穿过每一层。
但现实是,很多人即便认同 T3 技术栈的理念,真正上手时还是会在“配置”这道坎上卡住。比如 Next.js 14 的 App Router 和 Server Component 的边界在哪里、use client该加在哪个组件、tRPC 的 route handler 如何挂到app/api/trpc/[trpc]/route.ts、NextAuth 的 session 如何注入到 tRPC 的 context 里。这些如果靠查文档硬啃,半天就搭进去了;如果靠复制别人的模板,又容易继承一箩筐无关代码和历史包袱。
t3code项目的目标,就是把这一整条链路的最佳实践固化下来,并且用命令行把“约定优于配置”落地。你不需要理解每一步底层原理,也能得到经过了大量真实项目验证的骨架。这不是让你放弃理解原理,而是让工具替你解决确定性部分,把脑力花在真正有挑战的业务设计上。
1.2 为什么叫 t3code
这个名字其实点出了两层含义。第一层是技术栈:t3就是 T3 Stack 的简称,意味着生成的工程默认围绕 Next.js + TypeScript + Tailwind + tRPC 这一组合来构建。第二层是生产力工具:code不仅指项目代码,也暗示“生成代码”的能力。
我一直觉得,一个好的脚手架应该做到两件事:一是开箱即用,二是长期可维护。t3code用固定模板保证前者,用生成器和配置文件保证后者。它不像那些把成百上千个 feature 全塞进去的重量级框架,而是尽量保持精简、可替换、可理解。项目里任何一层你都可以抽出来换掉——比如不想用 Prisma 可以换成 Drizzle,初始化时就有对应的选项。这符合 T3 技术栈的精神:不做大而全的“全家桶”,只做小而实用的组合拳。
1.3 适合谁来用
我实际用下来的感受是,t3code在下面几个场景里尤其顺手。
独立开发者做 MVP 或内部工具时,没有比“一条命令起项目 + 一条命令建模块”更爽的体验。产品想法刚冒出来,最怕被环境配置淹没,t3code切掉了这一层负担,你可以直接把数据库模型写出来,把页面搭出来,集中精力验证业务是否成立。
小团队做项目统一时,t3code的固定目录结构和命名规范能减少很多无意义的讨论。以前我们团队几个人各建各的项目,连 router 文件放哪个目录都能吵一轮,后来统一了生成基线,新人看代码上手快,代码评审的重点也从“风格争论”转移到了“业务逻辑是否正确”。
进阶学习者也适合拿它当参考资料。因为生成出来的代码几乎没有注释、没有多余的示例业务,完全是一套按照官方推荐方式组合起来的代码库,很适合用来对照文档看“最佳实践长什么样”。如果你正在理解 tRPC 如何与 NextAuth 协同、Prisma client 单例怎么在 Next.js 开发环境下避免热重载时重复实例化,直接读生成的代码会比看分散的文档更快。
2. 技术选型解析:为什么是这条技术路径
2.1 T3 技术栈的核心优势
先说组合里的四个成员。Next.js 负责应用框架、路由和渲染策略,它的 App Router 让 Server Component 和 Server Actions 这些新能力有了用武之地;TypeScript 提供全链路静态类型,这是整套组合的基石;Tailwind 做样式,它的 utility class 方案和组件化开发天然适配;tRPC 则替代了传统 REST API 层,让前端直接“调用”后端函数,最大程度复用类型。
这一套组合的优势在于“类型穿透”。传统开发模式下,数据库表结构、后端 API 的定义、前端请求的返回类型,至少有三套需要手工维护的类型声明。改一个字段名,后端改了,前端忘了,跑起来才在控制台看到undefined或者某个data.map is not a function的报错。而 T3 组合里,类型从数据库到页面的路径几乎是一条直线:Prisma schema 定义模型 →prisma generate生成类型 → tRPC router 返回该类型 → 前端useQuery直接推导出返回值类型。中间不存在手工对齐的环节。
有人可能会问:为什么不用 REST + OpenAPI 生成客户端类型?也不是不行,但你需要额外配置 openapi-generator、维护 DTO 层,还要处理请求参数和响应包装的序列化规则,复杂度高出不少。对于团队内部服务或中等规模应用,tRPC 这条“少一层抽象”的路径明显更顺滑。而且 tRPC 不是不能做多端复用,只要保持 transceiver 按照同样的模式注册,也能服务于多个前端应用。
2.2 手写配置与 t3code 模板的实际差异
每次介绍脚手架,总有人坚持“手写一遍能更懂原理”。我不否认这一点,但要看场景。如果你只做一个项目,手动从头配确实能学到东西。如果你要做五个、十个项目,手动配置的边际成本就非常可怕了。关键的差异在下面几个维度。
目录结构上,t3code把约定直接写死在模板里。src/server/db.ts就是数据库实例,src/server/api/trpc.ts就是 tRPC 初始化文件,src/trpc/server.ts负责服务端类型调用,src/trpc/react.tsx负责客户端 hooks。任何人打开项目,不用看文档也能猜出文件的职责。
依赖选型上,t3code锁定的是一套经过兼容性测试的版本集合。它不会给你塞一个 Next.js 15 配一个 tRPC 10,那种版本错位产生的坑往往是新手最迷茫的。初始化过程中也能选择性去掉用不到的依赖,项目更干净,pnpm install更快。
环境变量上,模板自带.env.example,把DATABASE_URL、AUTH_SECRET、AUTH_URL这些最基础的变量提前备好,省得跑起来之后 500 报错才意识到少了环境变量。运行时还会用env.mjs做一次校验,变量缺失会给出明确报错。这个设计我特别喜欢,因为它把“配置错误”提前到了进程启动阶段,而不是等到某个接口被调用时才暴露。
2.3 端到端类型安全的落地逻辑
端到端类型安全听起来玄,拆开后其实就几条硬约束。第一,数据库层的类型由 Prisma 负责,prisma generate之后所有模型都有完整的 TypeScript 类型;第二,应用层接口由 tRPC 负责,每个 query 和 mutation 的输入用 Zod 校验,输出类型直接从返回值推导;第三,前端消费层由 React Query 配合 tRPC 客户端生成 hooks,类型从 server 自动流到组件里。
这个链路里最难理解的部分,或者说最容易找不到北的部分,就是 tRPC 的类型是如何从server传到client的。实际上它靠的是AppRouter这个抽象类型。在根路由文件里,你把所有子 router 聚合起来,然后在前端createTRPCReact<AppRouter>()时传入这个泛型。编译器见了这个泛型,就自动知道了远端有哪些 query、哪些 mutation、参数类型是什么、返回类型是什么。
有人担心类型安全绑得太紧会降低灵活性。我的观点是,要分清“灵活性”和“混乱”。如果你的系统真的需要跨语言微服务、需要和第三方开放平台对接,那一层 REST/OpenAPI 本身就是必要的,tRPC 不适合硬塞进去。但如果是内部全栈应用,类型安全不会拖慢你,反而能让你在改 schema 时更放心,因为编译错误会像探针一样扫出所有使用点。
3. 实操过程:从安装到跑起一个 demo
3.1 环境准备与安装
我用t3code在 Windows、macOS、Linux 上都跑过工程,最推荐的环境是 Node 20 LTS + pnpm。Node 18 也能跑,但跑 Next.js 14 时会有一些实验警告,不至于不能用,只是日志看着不干净。如果当前 Node 版本太老或者太新,建议用nvm切到 20。
t3code本身不需要全局安装,直接通过npx调用即可,这样不会污染全局环境,也能保证每次使用都是最新版:
npx t3code@latest new my-app执行后会进入交互式问答,需要回答下面几个问题:
| 提示项 | 可选值 | 我的建议 |
|---|---|---|
| 应用类型 | App Router / Pages Router | 新项目一律 App Router |
| 认证方案 | NextAuth / 无认证 / 自定义 JWT | 带登录业务选 NextAuth |
| 数据库适配器 | Prisma / Drizzle / 无 ORM | 关系型数据选 Prisma |
| 样式方案 | Tailwind / CSS Modules | 默认 Tailwind |
| 包管理器 | npm / pnpm / yarn / bun | 团队项目推荐 pnpm |
如果本机没有pnpm,可以通过corepack enable启用,或者直接选 npm,两种方式生成的代码没有区别。我之所以更推荐pnpm,是因为它在多项目场景下会复用全局缓存,磁盘占用低,安装速度也稳定。
3.2 初始化项目实战
我实际用一个叫做my-app的项目来演示整个流程。初始化时选择 App Router、NextAuth、Prisma、Tailwind、pnpm,生成后的目录结构大致如下:
my-app/ ├── prisma/ │ └── schema.prisma ├── src/ │ ├── app/ │ │ ├── api/trpc/[trpc]/route.ts │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── globals.css │ ├── server/ │ │ ├── api/ │ │ │ ├── root.ts │ │ │ ├── trpc.ts │ │ │ └── routers/ │ │ │ └── post.ts │ │ ├── auth.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── client.ts │ │ ├── react.tsx │ │ └── server.ts │ ├── env.mjs │ └── middleware.ts ├── .env.example ├── .eslintrc.cjs ├── next.config.mjs ├── package.json └── tsconfig.json注意.env.example里有几个关键变量需要填写:
# Prisma DATABASE_URL="postgresql://postgres:password@localhost:5432/my_app?schema=public" # NextAuth AUTH_SECRET="change-me-to-a-random-32-char-string" AUTH_URL="http://localhost:3000"复制成.env之后,把数据库连接串改成你自己的。如果没有本地 Postgres,可以用 Docker 快速起一个:
docker run --name my-app-db \ -e POSTGRES_PASSWORD=password \ -p 5432:5432 \ -d postgres:16然后安装依赖并启动:
pnpm install pnpm db:push pnpm dev这里解释一个细节:pnpm db:push是直接把 Prisma schema 推送到数据库,适合初期快速验证。如果之后需要维护迁移历史,改用pnpm prisma migrate dev --name init。
3.3 数据库模型与 Prisma 的实际连接
模板里默认提供的模型分两部分:一是 NextAuth 所需的User、Account、Session模型,二是给业务示例用的Post模型。你可以打开prisma/schema.prisma看看:
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id String @id @default(cuid()) name String? email String? @unique emailVerified DateTime? image String? accounts Account[] sessions Session[] posts Post[] } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) authorId String author User @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }这个 schema 的核心值得多看两眼。User和Post是一对多关系,posts数组和author关系字段必须成对出现;@updatedAt会在每次更新时自动写入当前时间,你不用在业务代码里手工维护。新增字段后,跑一下pnpm prisma migrate dev --name add_description生成迁移文件并同步数据库。
我第一次用这种模式时最不习惯的是:改完 schema 之后必须重新prisma generate,类型才会更新。如果你正在编辑器里写代码,prisma client 的类型不会自动刷新,必须跑命令。t3code在package.json里的postinstall脚本中一般会自动执行prisma generate,所以装完依赖那一刻类型就绪。但后续手动改 schema 时,别忘了这一步。
3.4 跑通第一个 tRPC API 流程
初始化成功后,src/server/api/routers/post.ts会预置一个简单的postRouter,里面包含一个createmutation 和一个listquery。它的作用不仅仅是示例,还是展示“类型安全链路”的最小闭环。
import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { db } from "~/server/db"; export const postRouter = createTRPCRouter({ list: publicProcedure.query(async () => { return db.post.findMany({ orderBy: { createdAt: "desc" }, take: 20, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1), content: z.string().optional() })) .mutation(async ({ input }) => { return db.post.create({ data: { title: input.title, content: input.content }, }); }), });前端想要调用它,只需要在客户端组件里用api.post.list.useQuery(),类型全部自动推断:
"use client"; import { api } from "~/trpc/react"; export function PostList() { const { data: posts, refetch } = api.post.list.useQuery(); const createPost = api.post.create.useMutation({ onSuccess: () => refetch(), }); return ( <ul> {posts?.map((post) => ( <li key={post.id}>{post.title}</li> ))} </ul> ); }跑完这个流程你就已经把整套全栈闭环打通了。不用写 fetch、不用定义 API 路径,也不用在前后端之间手写类型,这个体验一旦适应,真的回不去传统 REST 那套繁琐的开发方式。
4. 核心功能实操:代码生成与工程扩展
4.1 用 t3code 快速生成业务模块
项目跑通之后,关键的提效时刻来了。假设我要做“博客”模块,执行:
pnpm t3code generate module blog它通常会问你是fullstack还是api-only,选fullstack会生成整套前后端代码。生成器自动完成下面这些事:
- 在
schema.prisma中加入Blog模型,关系会自动挂到User上 - 生成一个迁移文件,但不会自行执行,需要你手动
prisma migrate dev - 生成
src/server/api/routers/blog.ts,包含 list、getById、create、update、remove 五个过程 - 在
src/server/api/root.ts中自动注册blogRouter - 在
src/app/blog下生成列表页、详情页、新建/编辑页 - 生成对应的客户端组件,比如
BlogForm、BlogList、删除按钮等
生成出的blog.ts路由大致长这样:
import { z } from "zod"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "~/server/api/trpc"; export const blogRouter = createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) => { return ctx.db.blog.findMany(); }), create: protectedProcedure .input(z.object({ title: z.string().min(1), body: z.string().min(1), })) .mutation(async ({ ctx, input }) => { return ctx.db.blog.create({ data: { title: input.title, body: input.body, authorId: ctx.session.user.id, }, }); }), });这个过程最大的价值是“权限默认被处理了”。create用了protectedProcedure,没有登录的用户根本无法调用,你不需要再写任何手动 session 判断。当然,这只是一个合理的默认值,如果你希望某个接口允许匿名访问,改成publicProcedure即可。
4.2 配置项速查表
t3code支持在项目根目录放一份t3code.config.ts来定制生成行为。我一般会这样写一份最小配置:
import { defineConfig } from "t3code"; export default defineConfig({ database: { orm: "prisma", provider: "postgresql", }, auth: { provider: "next-auth", }, naming: { routerFile: "camelCase", componentFile: "kebab-case", }, crud: ["list", "getById", "create", "update", "remove"], reactQuery: true, });| 配置项 | 作用 | 补充说明 |
|---|---|---|
database.orm | 选择 ORM 类型 | 目前默认 Prisma,预留 Drizzle 接口 |
auth.provider | 选择认证方案 | 影响生成路由时是否使用 protectedProcedure |
naming.routerFile | router 文件命名风格 | 建议camelCase |
naming.componentFile | 组件文件命名风格 | 建议kebab-case |
crud | 生成哪些 tRPC 过程 | 可以只生成list和getById |
reactQuery | 是否生成客户端 hooks | 关闭后只生成 server 类型 |
我建议团队把这份配置纳入代码审查范围,而不是让它成为“某个人电脑上的魔法文件”。配置文件一旦定下来,生成的代码风格就锚定了,之后每次generate都会保持一致。
4.3 自定义模板
内置模板覆盖不了团队特定场景时,t3code允许自定义 Handlebars 模板。把模板放到.t3code/templates目录,然后在生成时指定模板名即可。
比如我们团队所有删除按钮都要求带二次确认,那我就可以写一个remove-button.hbs:
export function {{name}}RemoveButton({ id }: { id: string }) { const remove{{name}} = api.{{module}}.remove.useMutation(); return ( <button onClick={() => { if (window.confirm("确定删除这条记录?")) { remove{{name}}.mutate({ id }); } }} > 删除 </button> ); }然后生成时这样指定:
pnpm t3code generate component BlogRemoveButton --template remove-button这里有个容易踩的坑:模板变量名的转换规则。模块名blog传入{{name}}时,生成器默认会帮你转成Blog用于组件名,转成blog用于变量名。但如果你在模板里直接使用{{name}}而不做命名约束,很容易生成blogRemoveButton这种不够规范的结果。我的建议是自定义模板里明确用{{pascalName}}、{{camelName}}这类预置变量,而不是依赖手写大小写。
4.4 构建与部署阶段的注意事项
t3code生成的工程在本地跑通后,部署到服务器前有几个点要检查。
先看看.env里是否设置了NEXTAUTH_URL或AUTH_URL,在服务器上要改成实际域名,不要用localhost。数据库连接串也一样,生产环境要用云数据库的连接地址,并确保端口能从服务器访问。
再看构建命令。通常执行pnpm build,如果构建过程中报“Page not found”或者“Module not found: Can't resolve @/xxx”,十有八九是tsconfig.json里的paths配置缺失或者baseUrl不对。t3code默认配好了@/*指向src/*,但如果有人手工删了或改动了,问题就来了。
最后是静态资源路径。如果你把应用部署到子路径下,比如https://example.com/app,需要在next.config.mjs里配置basePath: "/app",否则页面资源和 API 请求路径都会不对。
5. 常见问题与排查技巧实录
5.1 App Router 与 Pages Router 选错后的迁移成本
初始化时很容易一时冲动选 Pages Router,因为它看起来更“稳定”。但跑到中途想用 Server Component、next/image新特性时就会后悔。我在一个项目里被迫迁移过,那一次收获是:Pages Router 的getServerSideProps逻辑迁移到 App Router 并不简单,你得把每个页面拆成 Server Component + 子组件,数据获取函数要么放进 Server Component 里直接查询,要么用 Server Actions 包装。
所以我的建议很明确:新项目直接选 App Router。Pages Router 兼容 Sequelize 那样很多旧生态,但现在 Next.js 的核心演进都在 App Router 上,潮流很明确。如果已经生成错了,还是早点重建项目目录搬过去,越大改越痛。
5.2 tRPC 类型更新不及时
这是t3code项目里遇到频率最高的问题:改了router.ts中的 query 返回类型,前端useQuery的类型不变,甚至编辑器还报旧错误。多数情况不是代码的问题,而是 Next.js 和 React Query 的类型缓存没刷新。
我的标准排障三步:
rm -rf .next rm -rf node_modules/.cache pnpm dev如果还不行,检查tsconfig.json中的include是否覆盖了src目录,以及tsc是否用的是 Next.js 自带的构建流程。tRPC 的类型推导依赖root.ts里AppRouter的类型聚合,偶尔会因为 IDE 服务没有重载而滞后,重启 TypeScript 服务一般就能解决。
5.3 Prisma 引擎和 Node 版本不匹配
我踩过一个比较隐蔽的坑:本地 Node 版本太新,比如 23+,运行prisma generate时会出现 “unexpected data” 或引擎崩溃。Prisma 对 Node 版本要求比较保守,LTS 永远是最稳的选择。这时候用nvm use 20切回 LTS 再重新prisma generate就好。
有人会尝试设置PRISMA_ENGINES_MIRROR之类的环境变量去手动指定引擎源,但这属于临时方案,且不一定能解决版本兼容问题。我更建议从根上统一团队 Node 版本,配合.nvmrc文件,大家在同一版本下协作。
5.4 Docker 化部署时的数据库连接串问题
t3code生成的模板开发时直接用localhost连接数据库,但如果把应用也容器化,容器里的localhost指的是容器自身,不是宿主机。这时候数据库连接串要写成宿主机 IP,或者在同一个docker-compose.yml里用服务名互访。例如DATABASE_URL="postgresql://postgres:password@db:5432/my_app",这里的db是 compose 里 PostgreSQL 服务的名称。
另一个常见问题是容器启动时数据库还没就绪,导致prisma migrate失败。一种处理方式是在启动脚本里循环检测数据库可用性,或者用t3code模板里推荐的 migrate + start 流程,让应用启动前先跑迁移。
5.5 依赖安装慢或源异常
用 npm 安装全栈项目时,常常会遇到依赖安装极慢或者某个包拉不下来。我一般先用pnpm,它解析依赖更快,而且在同一台机器上多项目共享缓存。如果网络延迟还是很高,可以查一下当前 registry 配置:
pnpm config get registry npm config get registry如果指向了不稳定的公共源,可以考虑切换到更快的源,但我不建议在package.json里写死 registry,那会让其他同事克隆后一脸困惑。
提醒:不要在
package.json里手写一堆resolutions去强制升级某个子依赖的版本,除非你非常清楚它在供应链里所有依赖方的兼容性。我曾为修一个漏洞强行固定某个 Next.js 子依赖版本,结果把 SWC 相关的包弄崩了,构建直接失败。让依赖保持官方版本组合是更稳妥的选择。
6. 我的实操经验与使用建议
从第一次用t3code到现在,它已经成了我几个个人项目和团队项目的默认起点。我最大的体会是,它解决的不是“怎么写代码”的问题,而是“别把时间浪费在重复搭建”的问题。真正有创造性的部分仍然在自己手里,数据模型怎么设计、业务规则怎么划分、权限边界怎么定,这些一点都不会被工具替代。
如果让我给团队建议,我会强调三点。第一,把t3code.config.ts和schema.prisma看成项目里最重要的源文件,业务代码反而是派生产物。源文件稳定,生成的代码才稳定。第二,自定义模板不要一上来就追求大而全,先固化两三个真正高频的模块,用顺手了再逐步增加,别把生成器变成第二个需要维护的巨型系统。第三,版本升级一定要看t3code的更新记录,Next.js、tRPC、Prisma 这几个依赖升级节奏不一致,升级前不确认兼容性,生成出来的代码可能就“跑得起来但类型全乱套”。
最后分享一个小技巧:t3code generate module支持通过--crud list,getById这种参数来精准控制生成范围,不需要每次都生成全套。比如某些只读业务模块,我只想要列表和详情,就不让它生成一堆表单,省得之后还要删。这个选项在配置上也对应crud字段,团队成员可以按模块类型各取所需。工具用顺手的标志就是“你几乎忘了它的存在”,t3code对我来说已经到这个状态了。