最近在整理自己的全栈开发工具箱把好几个项目里反复出现的公共部分抽离出来做了一个叫 t3code 的项目模板。简单说t3code 就是基于 T3 Stack 技术栈——Next.js、tRPC、Prisma、Tailwind CSS、NextAuth——的一套完整工程化模板它把端到端类型安全的全栈开发体验固化成了标准动作让新项目的初始化时间从折腾大半天压缩到一顿饭的工夫。如果你正在纠结怎么组织一个前后端一体的 TypeScript 项目或者想尝试 tRPC 这种没有 API 文档的开发模式这篇文章会把这些年的实操经验都摊开讲。市面上关于 T3 Stack 的教程不少但大多数停留在跟着向导点几下的层面一旦遇到类型报错、鉴权失效、数据库连接不稳定这类真实问题资料就变得很零散。我写 t3code 的初衷就是想把这些坑提前填平顺便给后来者一份可以照着抄的作业。下面会把技术选型的思考、核心代码的实现逻辑、完整搭建流程以及我实际踩过的坑都过一遍希望你看完能直接上手而不是又收藏一个吃灰的链接。1. 先想清楚 t3code 到底解决什么问题1.1 做模板之前我先问了自己三个问题动手写 t3code 之前我认真列过一件事一个全栈项目模板到底该解决什么我的答案有三个。第一新项目的冷启动成本。每次开新项目都要重新配置 Next.js、接数据库、搭鉴权、配样式这些重复劳动既没有技术含量又容易出错。一个可靠的模板应该把地基打完让业务代码直接在上面长出来。第二团队协作的认知一致性。前端、后端、数据库如果各写各的接口文档一散联调就变成扯皮现场。如果一套技术栈能把类型从数据库一路推导到 UI 层很多沟通成本直接省掉bug 也能在编译期就暴露。第三代码质量的底线保障。模板不仅仅是能跑还应该在目录结构、错误处理、鉴权流程、数据库访问这些方面给出标准范式。别人看你的项目代码一眼就知道某些东西该放哪、该怎么写。这三个问题看起来简单但真正把它们落到代码层面牵扯到的细节远比想象中多。t3code 的每一层配置几乎都是为了回答其中一个问题而存在的。1.2 类型安全不是锦上添花而是省钱机器T3 Stack 的核心口号是类型安全优先初听像一句漂亮话但用久了会发现它真的在省钱。举一个最直接的例子传统前后端分离项目里后端改了个字段名前端可能到联调阶段才通过 500 状态码发现而在 t3code 这种 tRPC 模式里后端改了类型前端编译直接报错你根本来不及把 bug 带到浏览器里。我算过一笔账一个中等复杂度的后台管理系统因为类型问题导致的联调返工平均下来每次要耽搁两三个小时。模板里如果能把类型安全的链路打通一年省下来的时间足够再做一个小项目。这也是 t3code 把所有组件都收拢在 TypeScript 生态里的根本原因——类型就是契约契约在编译期就已经对齐了。2. 技术选型与整体架构思路2.1 四个核心组件为什么必须凑在一起很多人第一次看 T3 Stack 会疑惑Next.js 我能理解但 tRPC 不是和 REST、GraphQL 重复了吗Prisma 不就是个 ORM 吗为什么要塞进同一个项目其实这四个组件各自解决的是全栈链路上的不同环节凑齐了才闭环。Next.js 负责应用框架和渲染策略页面、路由、服务端函数都由它管理tRPC 干掉了 API 层的胶水代码让服务端函数被客户端直接调用同时全程类型推导Prisma 把数据库表映射成类型安全的模型查询结果天然携带类型Tailwind 负责 UI 层的快速构建配合自动补全能省掉大量切换上下文的时间。更关键的是这四个组件有一个共同的特性都优先支持 TypeScript。它们的类型定义不是事后补的而是底层就用 TS 写的。这意味着类型信息可以在数据库、服务端、客户端三层之间无损流动而不是像很多旧方案那样类型在某层被彻底抹掉后面全靠手写声明凑合。2.2 为什么 NextAuth 也值得放进默认配置鉴权是几乎所有真实项目的刚需但很多人会把它留到后面再说。我在 t3code 里一开始也犹豫过要不要集成 NextAuth毕竟多一个依赖组件就多一分体积和复杂度。后来实际项目里被鉴权反复折磨了几次就下定决心必须默认带上。原因很简单前期图省事不做鉴权后期补鉴权的成本高得惊人。你需要改接口、改前端路由守卫、改数据查询范围一堆页面全部要动。而 NextAuth 作为 Next.js 官方推荐方案和 App Router 的中间件机制配合得非常好配置好之后给页面加个登录保护只需要几行代码。t3code 里默认配好了 session 策略、数据库 session 存储和中间件保护路由你拿来就能用。我认为最实用的架构思路是默认完整按需裁剪模板里把常用的东西都给齐你的项目如果确实用不到某些部分删掉对应的依赖和目录即可。这比从一个空项目开始往上垒要快得多也安全得多。3. 核心实现细节与关键代码3.1 项目初始化create-t3-app 的取舍t3code 本身是基于 create-t3-app 二次改造的。官方脚手架已经解决了很多基础问题但它生成的项目比较素缺少我自己长期积累的目录约定、通用工具函数和错误处理范式。所以我保留了它的初始化方式然后在此基础上做了大量定制。初始化命令很简单但需要注意几个选项npm create t3-applatest my-app创建过程中会让你选择要包含的功能模块我强烈建议勾选 Next.js、tRPC、Prisma、Tailwind、NextAuth后面两个Drizzle 和 Expo按需选择。Drizzle 是另一个 ORM和 Prisma 二选一即可一般项目用 Prisma 更省心Expo 是移动端选项纯 Web 项目完全可以不选。创建完成后官方结构已经比较清晰但 t3code 在此基础上还会增加几个约定/server目录下按功能模块划分 domain 文件夹每个文件夹内部包含 schema、service、router 三件事/lib里放通用的工具函数/components/ui放基础组件。这样项目一长大也不会变成一堆文件的乱葬岗。3.2 tRPC 端到端类型安全从 router 到 proceduretRPC 的使用体验很独特它不发明新的协议也不生成额外的运行时代码纯粹靠 TypeScript 的类型推导把客户端和服务端缝起来。t3code 里的 tRPC 结构分三层server 端先定义一个根 router再往里面挂各个子模块的 router每个子 router 里定义若干 procedure一个 procedure 就是一个可被客户端调用的完整接口。// server/api/routers/todo.ts import { z } from zod; import { createTRPCRouter, protectedProcedure, publicProcedure } from ~/server/api/trpc; export const todoRouter createTRPCRouter({ getAll: protectedProcedure .query(async ({ ctx }) { return ctx.db.todo.findMany({ where: { userId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title, userId: ctx.session.user.id, }, }); }), toggle: protectedProcedure .input(z.object({ id: z.string(), done: z.boolean() })) .mutation(async ({ ctx, input }) { return ctx.db.todo.update({ where: { id: input.id }, data: { done: input.done }, }); }), });这里有几个关键点值得展开。procedure 分为publicProcedure和protectedProcedure区别在于后者强制校验 session没有登录的用户直接拒绝调用。input用 zod 做运行时校验类型和校验规则写在同一处杜绝数据层说合法、业务层说不合法的拧巴。客户端调用的时候类型是自动推导的。前端写api.todo.getAll.useQuery()你就能拿到类型完整的返回结果字段拼错直接红线。这种体验用久了真的回不去——你不再需要去看接口文档代码本身就是文档。3.3 Prisma 数据层schema 设计与迁移Prisma 的 schema 文件是整个数据层的源头。t3code 里我习惯把用户相关的模型、业务模型分文件管理然后通过prismaSchemaFolder特性让 Prisma 读取整个文件夹。下面是一段典型的 schemagenerator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) } model User { id String id default(cuid()) name String? email String unique emailVerified DateTime? image String? accounts Account[] sessions Session[] todos Todo[] } model Todo { id String id default(cuid()) title String done Boolean default(false) createdAt DateTime default(now()) userId String user User relation(fields: [userId], references: [id]) }开发环境默认用 SQLite零配置就能跑起来这对本地调试非常友好。生产环境我一般会换成 PostgreSQL只需要改 datasource 里的 provider 和 DATABASE_URL 环境变量业务代码一行都不用动。注意一个常见的坑改了 schema 后一定要执行迁移而且生产环境不要直接跑prisma db push那是开发环境偷懒用的。正确的流程是# 开发环境 npx prisma migrate dev --name init # 生产环境 npx prisma migrate deploymigrate dev会生成 SQL 迁移文件并同步数据库migrate deploy会把已有的迁移文件应用到生产库。两套动作的授权模型完全不同搞混了轻则报错重则把生产数据弄丢。3.4 NextAuth 集成Session 与受保护路由NextAuth 在 t3code 里承担两个职责一是管理登录态二是配合中间件保护路由。配置里有三个地方要重点关注。第一个是主配置写在server/auth.ts里import NextAuth from next-auth; import GitHub from next-auth/providers/github; import { PrismaAdapter } from next-auth/prisma-adapter; import { db } from ~/server/db; export const { handlers, auth, signIn, signOut } NextAuth({ adapter: PrismaAdapter(db), providers: [GitHub], session: { strategy: database }, callbacks: { session: ({ session, user }) ({ ...session, user: { ...session.user, id: user.id }, }), }, });provider 我这里以 GitHub 为例实际项目根据需求换成邮箱、微信、手机号都行。strategy: database会把 session 存进数据库配合 Prisma 的 Account/Session 模型服务端才能在 tRPC 的 protectedProcedure 里读到当前登录用户。第二个是中间件保护路由写在根目录的middleware.tsexport { auth as middleware } from /server/auth; export const config { matcher: [/dashboard/:path*, /admin/:path*], };这个文件的作用非常直白匹配到的路径未登录用户一律跳转登录页。注意要在自己的项目里实际确认一下中间件和路由前缀的匹配关系很多人配置了 matcher 却发现在某些嵌套路由上失效多半是路径写得不精确。第三处是 tRPC 上下文里的 session 注入。在server/api/trpc.ts里把 auth 返回的 session 放进 ctx后续所有 procedure 就能直接使用ctx.session。这样鉴权信息就能从 NextAuth 一路流到数据库查询条件里比如查当前用户的待办就非常自然。3.5 Tailwind 样式体系与 UI 组织样式部分 t3code 沿用 Tailwind但我额外定了一套小组件规范。基础按钮、输入框、卡片全部拆到components/ui业务组件不允许直接裸写按钮样式必须复用基础组件。这套规范一开始觉得啰嗦项目大了以后收益非常明显——改一个按钮圆角全站只要动一处而不是全局搜索七八种不同的 className。Tailwind 配置里我还加了cn工具函数基于 clsx 和 tailwind-merge用来处理条件样式拼接import { clsx, type ClassValue } from clsx; import { twMerge } from tailwind-merge; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }这玩意解决了一个经典痛点多个 className 互相覆盖。比如默认样式是px-4 py-2某个特殊场景要改成p-3直接用字符串拼接会同时存在px-4和p-3而tailwind-merge能识别出 p-3 是后面的自动把冲突的 px-4 py-2 去掉。这种细节看着小实际用起来特别提升开发幸福感。4. 完整实操流程从零到上线4.1 环境准备与版本要求动手之前先确认环境这一步能省掉后续一半的报错。我建议的版本组合是 Node.js 20 以上、npm 10 以上用 pnpm 也行但别在同一个项目里混用包管理器。Prisma 对 Node 版本有要求太老的版本装依赖容易失败Next.js 14/15 对 React 版本也有要求用官方 create-t3-app 生成的版本组合一般是最稳的。另外提前准备好一个空的 GitHub OAuth App因为 NextAuth 的 GitHub provider 需要客户端 ID 和密钥。这个步骤很多人卡住注意回调地址在本地开发是http://localhost:3000/api/auth/callback/github上线后要换成真实域名。4.2 创建项目与依赖安装执行初始化命令然后安装依赖npm create t3-applatest t3code-demo cd t3code-demo npm install初始化向导会问你要用哪几个模块按前面说的勾选即可。装完后复制.env.example为.env把数据库连接串、GitHub OAuth 的 ID 和密钥填进去。这里有一个我在很多项目里见到过的低级错误把.env文件提交到 Git。t3code 的模板里已经默认把.env写进.gitignore但如果你是自己改出来的项目务必检查一下。密钥泄露不是闹着玩的我见过不止一次因为 .env 被提交到公开仓库导致花了大半天轮换所有密钥。4.3 用 t3code 实现一个完整的待办功能模块做完初始化下一步就是把 t3code 跑起来做点实际的事。我以一个最简单的待办清单为例演示完整的增删改查链路。第一步设计数据模型。打开prisma/schema.prisma添加 Todo 模型关联到 User然后执行npx prisma migrate dev --name add_todo。第二步创建 tRPC 路由。新建server/api/routers/todo.ts参考前面贴的代码写上 getAll、create、toggle、delete 四个 procedure。注意所有涉及用户数据的操作都要用protectedProcedure并且查询条件强制带userId防止越权读取别人数据。第三步把路由挂到根 router。打开server/api/root.ts把 todoRouter 挂进去。这一步很多人会忘记导致客户端一直抱 404。第四步写前端页面。新建app/todos/page.tsx用useQuery和useMutation调用接口。tRPC 客户端已经自动生成好了直接 import 即可。use client; import { api } from ~/trpc/react; export default function TodosPage() { const utils api.useUtils(); const { data: todos, isLoading } api.todo.getAll.useQuery(); const createTodo api.todo.create.useMutation({ onSuccess: () utils.todo.getAll.invalidate(), }); const toggleTodo api.todo.toggle.useMutation({ onSuccess: () utils.todo.getAll.invalidate(), }); if (isLoading) return div加载中.../div; return ( div form onSubmit{(e) { e.preventDefault(); const form e.currentTarget; const input form.elements.namedItem(title) as HTMLInputElement; createTodo.mutate({ title: input.value }); form.reset(); }} input nametitle placeholder新待办 / button typesubmit添加/button /form ul {todos?.map((todo) ( li key{todo.id} input typecheckbox checked{todo.done} onChange{() toggleTodo.mutate({ id: todo.id, done: !todo.done })} / {todo.title} /li ))} /ul /div ); }这段代码的核心技巧是onSuccess: () utils.todo.getAll.invalidate()。mutation 成功后把缓存里的 getAll 数据标为失效react-query 会自动重新拉取最新列表。不需要手动改本地状态也不需要刷新页面省掉了一堆样板代码。第五步验证类型安全。试试故意把todo.title写成todo.titl编辑器应该立刻飙红色。这个反馈链路是整个 t3code 最值钱的地方。如果没报错检查一下 tRPC 客户端是否真的从同一个 server 类型推导很多人伪类型安全就是因为在客户端手写了类型声明把这条链路破坏了。4.4 部署阶段的关键调整开发完后部署也不能马虎。如果部署到 Vercel有几个点需要注意环境变量要重新填一遍数据库要用生产环境的连接串NextAuth 的回调地址改成正式域名Prisma 在构建时需要执行prisma generate很多部署失败都是因为这一步缺失。我的部署流程是先在本地跑一次生产构建npm run build确认没有类型错误再推送到部署平台。因为 t3code 是全类型安全的项目类型错误会在构建期暴露这反而是好事——总比用户点开页面看到白屏强。5. 常见问题与排查实录5.1 tRPC 类型不同步怎么办tRPC 的类型推导依赖服务端 router 的类型如果你改了 server 端代码客户端没反应最常见的原因是 dev server 没有重新加载。tRPC 的类型是编译期推导的理论上和热更新无关但实际使用中我自己遇到过几次t3code 里额外引入的类型入口文件比如server/api/root.ts被缓存了重启 dev server 就恢复正常。如果重启还不行那就检查是不是有人手动在客户端写了type Todo {...}之类的本地类型。一旦有手写类型介入整个推导链就被截断了。记住一句话t3code 的原则是类型只能有一个来源让 TypeScript 自己去找答案别帮它做决定。5.2 Prisma 连不上数据库Prisma 连接数据库报错的概率非常高但原因往往就那么几种。先确认 DATABASE_URL 格式对不对PostgreSQL 的格式是postgresql://USER:PASSWORDHOST:PORT/DATABASE密码里如果包含特殊字符要 URL 编码比如要写成%40。本地用 SQLite 的时候记得环境变量是file:./db.sqlite别手滑写成localhost:3306之类的。再一个高频错误是生产环境没有执行迁移表根本不存在migrate deploy一下就好。5.3 NextAuth 登录跳转异常、Session 一直拿不到NextAuth 出问题九成和 session 取法有关。在 t3code 里服务端代码拿 session 要用auth()或者 tRPC 的 ctx.session客户端拿 session 要用useSession()。如果你在客户端组件里直接调用服务端的 auth()那拿到的永远是 null。还有一个非常隐蔽的坑NextAuth 的secret没有配置时开发模式还能跑生产环境直接报错。t3code 模板里已经把这个变量预留了填一下.env里的NEXTAUTH_SECRET即可用openssl rand -base64 32生成一个强随机串。5.4 常见问题速查表现象可能原因解决办法客户端调用 tRPC 报 404路由没挂到 root router检查server/api/root.ts类型不更新dev server 缓存重启 dev server取消手写类型Prisma 查询报表不存在没执行迁移npx prisma migrate deploy登录后 session 为空客户端用了服务端方法改用useSession()构建时 prisma 报错未执行 generate构建前加prisma generateTailwind 样式覆盖失效className 冲突用cn()合并6. 一些实际操作中的体会t3code 从成型到现在我前后用它开了七八个项目最直观的感受是焦虑变少了。以前开新项目光是把数据库、鉴权、接口层接起来就要心虚几天现在模板一套、改改业务代码当天就能让用户看到能点能跑的东西。这种正向反馈对保持开发热情很重要。最后再分享一个小心得模板不是越全越好。t3code 默认带的东西我已经反复裁剪过但每个人的项目口味不同你可以大胆删掉自己用不到的部分。比如纯前端项目就用不着 Prisma直接去掉相关的依赖和目录别让用不到的东西增加认知负担。最好的模板是你敢动手改它而不是拿它当圣旨供着。如果你准备在自己的下一个项目里试试 t3code我建议从一个小而完整的业务模块开始比如我文章里写的待办功能。跑通一条增删改查的链路你就能体会到服务端和客户端共享类型的爽感。剩下的就交给时间去验证吧。