提到 t3code 之前先聊聊我最近被问得最多的一件事为什么身边的团队越来越多地把 Next.js 和 tRPC 绑在一起用答案其实很简单——类型安全这件事一旦端到端跑通开发体验是真的回不去了。t3code 是我维护的一套以 T3 Stack 为核心的全栈 TypeScript 项目脚手架工具链目标很明确用一条命令把 Next.js TypeScript tRPC Prisma Tailwind 这套组合跑起来省掉每次新项目都要重复的手工配置。你不需要记住几十个依赖的版本搭配也不需要对着官方文档一步步复制粘贴直接拿它生成一个能跑、能改、能部署的骨架。适合谁看如果你正准备用全栈 TypeScript 起一个新项目或者已经入坑 T3 Stack 但每次建项目都被配置折磨这篇文章应该能让你少走很多弯路。1. t3code 的定位为什么需要用一套工具来封装 T3 Stack1.1 T3 Stack 的四个组件到底解决了什么问题先把 T3 Stack 拆开看。Next.js 负责页面渲染和 API 路由TypeScript 负责全链路类型约束tRPC 让前后端共享类型定义Prisma 管数据库访问Tailwind 管样式。这五个东西单拎出来任何一个都不算新但组合在一起就产生了一个很微妙的化学反应从前端组件里调用后端接口时你不再需要手写接口文档也不再需要猜返回值到底是什么结构。我最早接触这套组合时也踩过弯。以前做全栈项目最痛苦的就是接口联调阶段后端把接口文档丢过来前端照着字段名一个个手动补类型稍不留神字段拼写错了一个运行时才发现。T3 Stack 的核心思路就是把“手动对接”这一步彻底干掉——你用 Prisma 定义好数据库模型tRPC 路由里写方法页面上调用时TypeScript 会直接把返回数据的类型推断到你的编辑器里。字段名错了标红。参数漏了标红。这种体验类比一下就像装修时有了全屋定制的设计图不需要现场跟工人反复比划尺寸。那 t3code 在里面扮演什么角色简单的说它把“选型决策”变成了“配置项”。你不需要自己纠结要不要用 tRPC、要不要上 Prisma直接按需勾选工具帮你把依赖版本、目录结构、基础代码全部准备好。1.2 t3code 的工作方式不是文档是可交互的脚手架很多脚手架的毛病是“生成完就结束”目录结构摆在那里但下一步该改哪里、怎么连接数据库、怎么加第一个接口还是靠你自己悟。t3code 从一开始的定位就不是单纯的“模板复制器”而是一条可交互的开发流水线。它内部实际拆成了三步第一步是收集需求命令行里问你项目名叫什么、要不要鉴权、数据库类型选什么第二步是生成骨架按模块组合渲染出对应的目录和文件第三步是安装依赖并做初始化检测比如检查 Node 版本、数据库连接串是否合法、环境变量是否齐全。这样做的好处是同一个工具既能服务第一次接触 T3 Stack 的新手也能服务已经有固定习惯的老手。新手可以直接用默认模板跑通第一个项目老手则在交互式选区里把不需要的模块关掉尽量保持项目干净。1.3 社区里已有脚手架为什么还要维护自己的封装可能有人会问社区里已经有 create-t3-app 这类工具了自己再折腾一套 t3code 是不是重复造轮子我的回答是工具的本质是给工作流定规则而每个人的工作流多少有点差异。create-t3-app 解决的是“从无到有”的问题我需要的还有“从有到快”的部分比如自动生成一个可运行的 CRUD 示例、内置 Dockerfile、部署配置模板以及针对团队协作的一些约束。t3code 并不是要替换某个现成工具更像是在我自己的项目实践中沉淀出来的一套约定。它把 CRUD 生成、环境变量管理、Docker 部署三块我最常重复做的事情也收编进来了。所以这篇文章里的操作记录本质上也是我日常开发中反复执行的那套流程。2. 环境准备与初始化从命令跑到看到页面2.1 环境准备先解决 Node 和数据库这两个拦路虎在实际初始化项目之前有几个前置条件需要确认缺了哪一个都会在后面的步骤里突然跳出来报错。第一是 Node.js 版本t3code 要求 Node.js 18 及以上因为 Next.js 14 和 tRPC 10 的某些特性在老版本上跑不动。第二是包管理器我建议直接用 pnpm它比 npm 的磁盘占用低、安装速度快而且在处理依赖提升问题的时候更规范实测下来遇到“幽灵依赖”的概率小很多。数据库方面如果你选择启用 Prisma那本地至少得有一个可用的数据库实例。最简单的方案是用 Docker 起一个 PostgreSQLdocker run --name t3code-db -e POSTGRES_USERt3code -e POSTGRES_PASSWORDt3code -e POSTGRES_DBt3code -p 5432:5432 -d postgres:16这里要注意端口冲突的问题。如果本机 5432 已经被占用可以把宿主端口映射成 5433后面连接串里对应改成 localhost:5433 就行。不想用 Docker 的本地安装 PostgreSQL 或者用 SQLite 先顶着也可以不过生产环境还是建议 PostgreSQL尤其是后续要上并发写入和复杂查询的场景。环境变量这块是新手最容易翻车的地方。t3code 生成项目后会自动创建一个 .env.example 文件你需要先把它复制成 .env再逐项填写DATABASE_URLpostgresql://t3code:t3codelocalhost:5432/t3code?schemapublic NEXTAUTH_SECRET随便用 openssl rand -base64 32 生成一个 NEXTAUTH_URLhttp://localhost:3000NEXTAUTH_SECRET 一定不要默认值否则部署到线上会有安全问题。生成随机密钥的命令很简单openssl rand -base64 32。2.2 初始化一个真实项目命令行交互过程实录前置条件检查完就可以开始正式初始化了。t3code 的入口命令长这样npx t3code init my-blog执行后工具会先拉取最新模板然后进入交互式问答。几个核心选项我实测下来是这样的项目名称默认取参数里的 my-blog也可以直接回车跳过。需要鉴权模块吗选 Yes 会引入 NextAuth.js并提供一套基于 Credentials 登录的最小示例。启用数据库吗选 Yes 并指定 PostgreSQL 后工具会生成 Prisma schema 和一条示例迁移。使用 Tailwind CSS 吗选 Yes 会把样式相关的配置全部预置好。启用 ESLint 和 Prettier 吗我的建议是都启用团队协作时统一代码风格会省下大量不必要的冲突。问答结束后工具会先创建目录再执行安装依赖。整个流程看网速和镜像源通常两三分钟到十几分钟不等。依赖安装完成后控制台会提示你进入项目目录并启动开发服务cd my-blog pnpm dev打开 http://localhost:3000正常情况下会看到一个简单的首页同时终端里会打印出 tRPC 健康检查的日志。走到这一步说明骨架已经完全跑通了。2.3 生成后的目录结构逐层拆解t3code 生成的目录结构我整理一份高频核心目录如下my-blog ├── prisma │ └── schema.prisma # 数据模型定义 ├── public # 静态资源 ├── src │ ├── pages │ │ ├── api │ │ │ └── trpc │ │ │ └── [trpc].ts # tRPC 的 HTTP 入口 │ │ └── index.tsx # 首屏页面 │ ├── server │ │ ├── api │ │ │ ├── routers # 业务路由都放这里 │ │ │ └── trpc.ts # tRPC 根路由与上下文 │ │ └── db.ts # Prisma 客户端单例 │ └── styles │ └── globals.css # Tailwind 入口样式 ├── .env # 本地环境变量 ├── .env.example # 环境变量模板 ├── docker-compose.yml # 本地数据库编排 └── Dockerfile # 多阶段构建这个结构的核心思想是“server 目录只放后端逻辑pages 目录只做视图和路由”。第一次看可能会觉得文件比普通 Next.js 项目多了一些但只要记住一条主线页面发起请求 → tRPC 入口接收 → routers 里处理业务 → Prisma 访问数据库整条链路就清楚了。这里我强烈建议不要新项目一上来就乱改目录结构。工具生成的布局是经过实践验证的等你对每个文件的作用足够熟悉之后再按团队习惯调整也不迟。3. 跑通第一个 CRUD数据模型、tRPC 路由与页面调用3.1 数据模型先行用 Prisma Schema 定义 Todo项目能跑起来只是第一步真正体现 t3code 便捷性的是从零写一个功能。我用一个最经典的 Todo 示例来完整过一遍。首先打开 prisma/schema.prisma定义数据模型model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt }这里我把 id 设计成 String 并用 cuid 生成而不是自增整数主要原因是分布式环境下字符串 ID 不容易产生冲突而且泄露出去后不会暴露业务数据量。定义好模型后执行迁移命令pnpm prisma migrate dev --name init_todo这个命令会做三件事生成一份迁移 SQL、在数据库里执行迁移、重新生成 Prisma Client 类型。执行完你会在 prisma/migrations 目录下看到带时间戳的 SQL 文件这就是数据库结构的“版本记录”团队协作时大家都能同步。3.2 tRPC 路由从 query 到 mutation有了数据模型接着在 tRPC 路由里暴露操作能力。t3code 把业务路由统一放在 src/server/api/routers 下我新建了一个 todo.tsimport { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; import { db } from ~/server/db; export const todoRouter createTRPCRouter({ list: publicProcedure.query(async () { return db.todo.findMany({ orderBy: { createdAt: desc }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ input }) { return db.todo.create({ data: { title: input.title }, }); }), toggle: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) { const todo await db.todo.findUnique({ where: { id: input.id } }); if (!todo) throw new Error(Todo 不存在); return db.todo.update({ where: { id: input.id }, data: { completed: !todo.completed }, }); }), delete: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ input }) { await db.todo.delete({ where: { id: input.id } }); return { success: true }; }), });这里有几个值得展开的点。第一publicProcedure 表示无需登录就能调用项目接入鉴权后可以把部分操作改成 protectedProcedure。第二zod 的 input 校验是运行时的兜底就算有人绕过前端直接请求接口非法参数也会在这一层被拦截。第三这个文件本身没有直接暴露 HTTP 路由真正暴露入口的是 src/pages/api/trpc/[trpc].ts它把所有 router 合并统一挂载。定义好 todoRouter 后要把它挂到根路由上。打开 src/server/api/root.ts在 createCallerFactory 创建实例时注册export const appRouter createTRPCRouter({ todo: todoRouter, });这一步很容易漏掉。漏掉之后页面上调用 todo.list 会直接报“找不到路由”的运行时错误而且类型检查不会给你任何提示。3.3 页面组件里怎么调用后端数据模型和后端路由就绪接下来就是最爽的部分前端调用。在 src/pages/index.tsx 里直接用 tRPC 的 hooksimport { api } from ~/utils/api; export default function Home() { const utils api.useUtils(); const { data: todos, isLoading } api.todo.list.useQuery(); const createTodo api.todo.create.useMutation({ onSuccess: () utils.todo.list.invalidate(), }); const toggleTodo api.todo.toggle.useMutation({ onSuccess: () utils.todo.list.invalidate(), }); if (isLoading) return div加载中.../div; return ( div classNamep-8 form onSubmit{(e) { e.preventDefault(); const form e.currentTarget; const title new FormData(form).get(title) as string; createTodo.mutate({ title }); form.reset(); }} input nametitle classNameborder p-2 / button typesubmit classNameml-2 bg-blue-500 p-2 text-white 添加 /button /form ul {todos?.map((todo) ( li key{todo.id} classNameflex gap-2 py-1 input typecheckbox checked{todo.completed} onChange{() toggleTodo.mutate({ id: todo.id })} / span{todo.title}/span /li ))} /ul /div ); }注意 mutation 完成后的 invalidate 调用它的意思是“让列表重新拉取”。这是 tRPC 生态里很巧妙的设计你不需要手动改本地缓存状态只要声明数据失效框架会自动重新请求。对比传统 REST 接口这里省掉了大量“请求完手动刷新列表”的样板代码。页面跑起来后打开浏览器添加一条 Todo数据库里马上就能查到。整个过程类型是全程打通的createTodo.mutate 的参数类型直接来自 zod 输入定义todos 的元素类型直接来自 Prisma 模型。字段写错、类型对不上编辑器里当场报错绝对不带拖延的。3.4 自动生成的鉴权示例为多用户功能预留扩展点t3code 在启用鉴权模块后会额外生成一套最小的 NextAuth 配置并在 tRPC 上下文里注入 session 信息。先看服务端上下文的部分import { getServerSession } from next-auth; import { authOptions } from ~/server/auth; export const createTRPCContext async (opts: { headers: Headers }) { const session await getServerSession(authOptions); return { session, db, ...opts, }; };接着在 trpc.ts 里可以定义一个受保护的过程import { initTRPC, TRPCError } from trpc/server; export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { ...ctx, user: ctx.session.user } }); });这样在 todoRouter 里把创建和删除改成 protectedProcedure就可以保证只有登录用户才能操作。实际的用户体系接入还会涉及登录页、回调地址、会话过期等问题但脚手架给你的是一套能跑通的最小闭环大多数业务都可以基于这个闭环去扩展。4. 构建与部署开发完之后的完整交付链路4.1 常用脚本一览开发阶段常用的命令其实不多我把 t3code 项目里最核心的几个脚本整理成了一张速查表命令作用备注pnpm dev启动开发服务默认端口 3000pnpm lint运行 ESLint 检查提交前建议跑一遍pnpm build执行 Next.js 生产构建构建前会自动做类型检查pnpm start启动生产服务需要先 buildpnpm prisma migrate dev开发环境执行迁移会自动重新生成客户端pnpm prisma studio可视化查看数据库数据浏览器打开数据管理界面构建环节有一个常见认知误区pnpm build 不只是打包前端资源它还会对 tRPC 的路由做一次全量类型校验。如果服务端某个 procedure 返回类型有问题构建时就会直接失败。这对质量保障来说是好事但也意味着你在修改路由后不能只依赖开发模式的编译还是得上线前跑一次完整构建。4.2 Dockerfile 多阶段构建自托管部署是 t3code 默认支持的场景。项目根目录预置的 Dockerfile 采用多阶段构建实际生产里我基本不调整它FROM node:20-alpine AS base FROM base AS deps WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable pnpm install --frozen-lockfile FROM base AS builder WORKDIR /app COPY --fromdeps /app/node_modules ./node_modules COPY . . RUN npx prisma generate RUN pnpm build FROM base AS runner WORKDIR /app ENV NODE_ENVproduction COPY --frombuilder /app/.next ./.next COPY --frombuilder /app/node_modules ./node_modules COPY --frombuilder /app/package.json ./package.json EXPOSE 3000 CMD [pnpm, start]这里最关键的步骤是 npx prisma generate 必须在 builder 阶段执行否则 Next.js 服务端会找不到 Prisma Client 的生成代码。另一个容易踩坑的是如果你的 Prisma schema 里用了 PostgreSQL 的某些扩展在容器里还要额外安装对应的系统依赖比如 openssl 和 ca-certificates。构建完成后可以用 docker compose 把应用和数据库一起拉起来docker compose up -d --buildt3code 生成的 docker-compose.yml 默认包含 app 和 db 两个服务app 依赖 db 的健康检查通过后才启动。这样能避免应用启动时数据库还没就绪导致的连接失败。4.3 部署到托管平台与自托管的关键配置如果不想自托管部署到 Vercel 是最省心的方案。但有两个配置容易漏。第一是 Vercel 项目设置里的 Root Directory 要指向项目根目录别把 monorepo 的根目录当成了项目目录。第二是环境变量要在 Vercel 后台上手动添一遍.env 文件不会自动上传。自托管则要注意反向代理层。Nginx 转发到 3000 端口时如果部署在子路径下需要额外配置 Next.js 的 basePath否则静态资源的路径会全部 404。另外 NextAuth 部署后如果出现“回调地址不匹配”的问题优先检查 NEXTAUTH_URL 有没有配成线上实际域名这个字段在本地开发时往往被忽略等一上生产就立刻暴露。5. 实操中高频踩坑问题现象、排查思路与解决办法5.1 tRPC 类型不同步导致编辑器报错现象是修改了后端某个 procedure 的返回结构但前端调用处的类型没有更新编辑器仍然使用旧类型。这个问题的根源在于 tRPC 的类型是从服务端代码推断出来的而 Next.js 的 dev server 在增量编译时偶尔会出现类型缓存没刷新的情况。解决办法很朴素重启 dev server。大多数时候重启一次类型就同步了。如果没有解决检查你的编辑器是不是开了多个 TS Server 实例手动重启一下 TS Server 通常也能解决。5.2 Prisma 数据库连接失败这个问题的报错形式千奇百怪常见的有 Unable to connect to the database、数据库不存在、鉴权失败。我的排查套路是从连接串入手先确认 .env 里的 DATABASE_URL 是不是真的生效。因为 t3code 用 dotenv 管理环境变量如果 .env 文件不存在或者字段名拼写不精确Prisma 会静默使用默认值连接失败也就不奇怪了。还有一个隐蔽的坑使用 Docker 跑数据库时容器端口映射错误。确认宿主端口和容器端口别搞反了。连接串里写 localhost:5432 但 Docker 映射到 5433自然连不上。真正确认数据库可达可以手动跑一条命令pnpm prisma db execute --stdin SELECT 1能返回结果就说明 Prisma 层面的连接没问题后续问题就基本锁定在业务代码了。5.3 Tailwind 样式不生效t3code 默认启用了 Tailwind但如果你往项目里引入了 src 目录之外的文件或者新创建了某个特殊后缀的文件样式可能会突然失效。原因基本是 Tailwind 的 content 配置没有覆盖到对应文件路径。我在 tailwind.config.ts 里推荐的配置是这样的content: [ ./src/pages/**/*.{ts,tsx}, ./src/components/**/*.{ts,tsx}, ],如果自定义了类似 .tsx 之外的文件类型记得把后缀加进 content然后重启 dev server 并清一下浏览器缓存。5.4 生产环境 NextAuth 回调地址报错本地一切正常部署到服务器后一登录就报 Redirect URI 不匹配。这个问题的本质是 NextAuth 校验回调地址是否在环境变量的允许列表里。生产环境必须确认两点NEXTAUTH_URL 与线上访问域名完全一致协议必须带 https如果用了自定义回调地址还需要在 authOptions 的 providers 配置里显式声明。Vercel 部署时如果要使用 VERCEL_URL 作为环境变量需要特别小心它和实际域名可能不带协议尽量直接用 NEXTAUTH_URL 配置完整地址。5.5 Docker 部署后页面能打开但接口全部 500这个现象通常不是 tRPC 业务代码的问题而是容器内缺少 Prisma 引擎所依赖的系统库。尤其使用 alpine 镜像时缺少 openssl 和 libssl 会导致 Prisma 在运行时无法初始化而构建阶段因为是纯 TypeScript 编译所以不会暴露。解决办法是 Dockerfile 的 runner 阶段安装系统依赖或者在基镜像选择上放弃 alpine直接用 node:20-slim。写在最后文章到了这里t3code 从初始化到部署从 CRUD 到排错基本都过了一遍。我个人在实际使用中最大的体会是这套工具省下来的不仅是配置时间更是把技术选型的决策成本提前固定下来了。当你不再需要反复折腾“该用哪个 ORM、要不要上 tRPC”的时候精力就能更多放到业务逻辑本身。最后再分享一个小技巧拿到脚手架生成的项目之后先把默认的示例路由跑一遍再动手改。确认整条链路没问题再往里加自己的业务代码。这个习惯看起来浪费时间实际却能帮你把“环境问题”和“代码问题”清楚切割开。如果哪天你在接入 t3code 时发现某个坑我上面没写到也欢迎回头再翻翻这篇文章的评论区咱们一起把这份实践清单补全。