t3code:基于Next.js、TypeScript、tRPC和Prisma的全栈脚手架实战
如果你也是从零搭过三次以上全栈项目的人大概率能理解我为什么会花两个周末写 t3code。t3code 并不是一个新框架它是一套面向现代 Web 应用的起步模板和代码生成方案把 Next.js、TypeScript、tRPC、Prisma 这些当前全栈开发里出镜率极高的东西整合在一起让你在五分钟内拿到一套带鉴权、带数据库表、带端到端类型安全链路的完整骨架。它适合独立开发者也适合小型前端团队——只要你的目标是快速验证业务而不是把时间耗在 webpack 配置、Prisma Client 生成和手写 API 类型定义上。下面我把 t3code 的设计思路、技术选型、完整跑通步骤和踩过的坑全部摊开讲希望对你正在做的事情有参考价值。1. 项目定位与设计思路1.1 从零搭项目到底有多痛我大概在三个正式项目里经历过一个完全相同的开场先初始化 Create React App然后装 axios、装 redux-toolkit、装 react-router再写一堆api/types.ts手动维护接口返回数据的类型。前端定义一个Todointerface后端再写一遍同样的字段数据库模型里再写第三遍。一旦后端给createdAt加了deletedAt或者改了一个字段名前端项目就会立刻报错而且报错的地方往往不是改字段的那个文件而是某个页面在.map()或者.filter()调用处突然出现类型不匹配。改三处、缺一处最后只能靠“全局搜字段名”来补救。真正让我抓狂的是认证和权限。登录、刷新 token、处理 401、把用户信息塞进全局状态这一套在每个项目里都长得差不多但每次都要重新写。更不要说新建一个字段后还得同步去 React Query 的 queryKey、Router 的输入解析、数据库迁移文件里做一堆琐碎操作。这些东西不难但非常消耗精力和耐心。我想要的不是一套“标准答案”而是把一个经验证过的结构直接复制过来只改业务部分。1.2 为什么叫 t3codeT3 最早指 TypeScript、Tailwind 和 tRPC后来被社区扩展成“T3 Stack”一般会再加上 Next.js 和 Prisma。t3code 里的 code 则强调一点我们要生成的不是文档也不是只存在于 PPT 里的架构图而是能直接pnpm dev跑起来、能写进实际业务的代码。它本质上是一套“脚手架 代码生成器 约定式目录结构”。我把它定义成三层能力初始化层通过一条命令拉取项目模板自动完成依赖安装、目录生成、环境变量检查和 Prisma Client 生成。代码生成层通过交互式命令创建新的数据模型、新的 tRPC Router、新的表单页面自动把 schema 和类型串起来。约定层固定 server 与 client 的代码边界、数据库访问方式、鉴权注入方式让团队新成员看一眼目录就知道该往哪里加代码。这三层的目标是解决我心中最核心的痛点类型安全链路每次都要手工搭建为什么不把它变成一条生产线。1.3 它不是什么很多人听到“脚手架”会想到 create-react-app听到“代码生成器”会想到低代码平台。t3code 跟它们都不太一样。它不框架不会接管你的组件写法或者是路由定义方式你依然在用 Next.js 原生能力。它不是 ORM虽然默认集成了 Prisma但你可以换成 Drizzle只影响数据层代码。它也不是“代码托管平台”生成的代码完全归你所有没有运行时闭源 SDK。说得直白一点t3code 是一个胶水层它做的是把多个成熟工具以正确的方式粘在一起并且把粘合处容易出错的部分自动化。我见过很多项目败在“最佳实践”打满的全栈样板房上——代码结构太重、抽象太多、新手根本不知道从哪开始。t3code 的设计原则是“够用就好”一个 Todo 功能能跑通你的业务也能照着同样的路径跑通。它不追求大而全只保证链路完整、改起来顺手。2. 工具链选型与关键技术拆解2.1 一个端到端类型安全链路意味着什么传统方案里前端调一个列表接口通常是这么干的前端定义Todo[]后端写GET /api/todos响应回来之后还要写.map(item ({...}))做数据清洗。中间任何一个环节都可能因为字段名不一致而出现运行时错误——接口能访问到但数据是undefined。t3code 默认采用的组合是Next.js 负责页面和 API 宿主环境TypeScript 作为全链路静态类型语言tRPC 负责前端到后端的函数调用Prisma 负责数据库访问并生成数据库层的类型。这样从 React 组件里的useQuery到 tRPC procedure 里的input再到 Prisma 的findMany所有数据的形状都被 TypeScript 自动推导。你不需要写额外的接口定义文件改一个后端 Prisma 字段前端调用的地方会立刻出现类型报错。这种“改一处、编译器帮你查全部”的体验在大型业务里会大大减少不必要的联调成本。2.2 为什么选这四件套而不是别的我并不是只看重潮流而是仔细比较过三类方案。方案类型安全强度开发效率学习成本适用场景t3code 默认方案Next.js TS tRPC Prisma强端到端自动推导高不写接口层中需要理解 tRPC 概念中大型全栈应用、团队协作传统 REST Redux-thunk Axios弱需要手动同步类型中每次新接口都要写一堆胶水低大多人熟悉接口简单、前后端分离的团队GraphQL Apollo强常见框架有 codegen中需维护 schema 和 resolver高GraphQL 概念多多端复用、复杂查询场景我最终倾向 tRPC并不是因为 REST 不好而是因为 tRPC 在“全栈单一代码库”场景里能把类型同步成本压到最低。当你的前端和后端在同一个 monorepo 里由同一批人维护时为每个接口手动写 OpenAPI 定义再生成客户端类型确实不如直接让函数跨越网络边界来得直接。Prisma 在数据层的地位也一样它用schema.prisma做单一事实来源你在 schema 里定义的每一个模型都会同步生成对应的 TypeScript 类型。再加上迁移工具数据库结构和代码结构能保持一致。把迁移、种子数据和类型生成作为一个整体看待会让交付过程顺畅非常多。2.3 核心链路从 Prisma Schema 到页面渲染我直接拿 Todo 功能拆解。先建立数据模型model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt }跑一次pnpm prisma migrate dev会在数据库里建表并自动生成PrismaClient的类型。接下来在服务端创建一个 tRPC Routerimport { z } from zod; import { createTRPCRouter, publicProcedure } from ~/server/api/trpc; export const todoRouter createTRPCRouter({ list: publicProcedure.query(async ({ ctx }) { return ctx.db.todo.findMany({ orderBy: { createdAt: desc } }); }), add: publicProcedure .input(z.object({ title: z.string().min(1).max(100) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title } }); }), toggle: publicProcedure .input(z.object({ id: z.string(), completed: z.boolean() })) .mutation(async ({ ctx, input }) { return ctx.db.todo.update({ where: { id: input.id }, data: { completed: input.completed }, }); }), });前端组件里你不需要知道接口 URL也不需要关心 JSON 字段命名use client; import { trpc } from ~/trpc/client; export function TodoList() { const { data, isLoading } trpc.todo.list.useQuery(); const utils trpc.useUtils(); const addTodo trpc.todo.add.useMutation({ onSuccess: () utils.todo.list.invalidate(), }); if (isLoading) return p加载中…/p; return ( ul {data?.map((todo) ( li key{todo.id}{todo.title}/li ))} /ul ); }当schema.prisma中Todo模型新增一个priority字段后list的返回类型会自动包含它前端data[0].priority也会立刻有类型补全。这就是我说的“端到端类型安全”。它不会在运行时帮你兜底但能在开发阶段把最常见的字段错配拦截在编译之前。3. 从零跑通 t3code实操步骤与关键配置3.1 环境准备与初始化命令建议使用 Node.js 18 或更高版本包管理器选 pnpm原因很简单pnpm 的硬链接机制能在 monorepo 和大型依赖树里显著减少磁盘占用安装速度也有优势。另外需要装好 Git。数据库方面你不需要一开始就连接远程数据库直接让 t3code 使用 SQLite 文件把流程跑通等业务稳定后再切到 PostgreSQL。初始化命令是这样的pnpm create t3codelatest my-awesome-app因为包名和版本可能会迭代更稳妥的方式是使用npx t3codelatest init。命令执行后会出现交互式向导让你选择是否启用 Prisma、是否启用 NextAuth、是否使用 Tailwind CSS以及是否需要 ESLint 和 Prettier 的初始配置。首次体验建议全部勾选这样你能看到一整套完整链路。初始化完成后依次执行cd my-awesome-app pnpm install pnpm prisma migrate dev pnpm devprisma migrate dev会创建数据库和第一张示例表并自动生成 Prisma Client。接着访问http://localhost:3000应该能看到一个带登录入口和后端接口调用的页面。此时整个骨架已经能跑了。3.2 目录结构逐层拆解t3code 生成的目录结构不是随意摆放的它遵循“服务端代码与客户端代码物理隔离”的原则my-awesome-app/ ├── prisma/ │ ├── schema.prisma │ └── seed.ts ├── src/ │ ├── app/ │ │ ├── page.tsx │ │ ├── layout.tsx │ │ └── api/ │ │ └── trpc/ │ │ └── [trpc]/ │ │ └── route.ts │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ │ ├── todo.ts │ │ │ │ └── root.ts │ │ │ ├── trpc.ts │ │ │ └── auth.ts │ │ ├── db.ts │ │ └── auth.ts │ ├── trpc/ │ │ ├── server.ts │ │ ├── client.ts │ │ └── react.tsx │ ├── env.mjs │ ├── styles/ │ └── server-only.ts └── package.jsonprisma/schema.prisma是数据库模型的唯一事实来源改表结构先改这里。src/server/下面的东西只运行在 Node.js 环境不能把它们引入客户端代码。src/trpc/是前后端共享的类型工具库。server.ts负责创建 tRPC 实例并注入鉴权和数据库上下文client.ts负责在前端创建类型化的调用对象。src/app/api/trpc/[trpc]/route.ts是 Next.js App Router 与 tRPC 的连接器外部 HTTP 请求会进入这里再转发给对应的 procedure。第一次看到这么多文件容易觉得多但绝大多数文件是不需要改的。你要改的业务入口只有两个prisma/schema.prisma和src/server/api/routers/再加对应的前端页面。其他文件是基础设施。3.3 新建一个业务模块的正确步骤很多朋友第一次用 t3code 会问我想加一个“Post”表、然后做一个发布文章的功能该动哪些文件我的建议是严格按这个顺序操作在prisma/schema.prisma中新增Post模型存好草稿后执行pnpm prisma migrate dev。在src/server/api/routers/post.ts中新建postRouter把list、create、detail等 procedure 写好。在src/server/api/root.ts中把postRouter挂到appRouter的post键上。在前端页面里通过trpc.post.list.useQuery()调用。这套流程之所以高效是因为每一步之间有明确的“依赖关系”数据库类型变了tRPC 返回类型自动变tRPC 类型变了前端useQuery的data类型自动变。你不需要担心“忘记更新类型定义”因为类型定义就是由代码推导出来的。我在实际项目里把“新增一个完整 CRUD 接口”的时间压缩到了十分钟以内。十分钟里有三分钟花在想业务字段上五分钟左右等 Prisma 迁移和类型生成真正写代码的时间可能只有两分钟。这种体验在传统 REST 架构里很难实现。3.4 环境变量与鉴权配置t3code 会生成一个.env.example文件首次使用需要复制为.env并填入必要变量。关键变量包括DATABASE_URLfile:./dev.db NEXTAUTH_SECRETyour-secret-key NEXTAUTH_URLhttp://localhost:3000如果启用了 NextAuthNEXTAUTH_SECRET可以直接用openssl rand -base64 32生成。部署到 Vercel 时需要把DATABASE_URL切换成托管数据库的连接串同时注意连接池参数。比如 PostgreSQL 生产环境最好开启pgbouncertrueDATABASE_URLpostgresql://user:passwordhost:5432/db?pgbouncertrue这不是 t3code 特有的配置而是很多服务端渲染应用都会遇到的坑。需要提醒的是不要在.env文件里保存任何前后端共享的密钥。任何放到客户端 bundle 里的东西都等于公开。4. 常见问题与排查技巧实录4.1 高频错误速查表下面这些报错是我在多个项目里反复看到的很多来自社区反馈也有我自己踩过的。整理成一张表方便你直接对照。错误现象根本原因处理办法PrismaClientInitializationErrorPrisma Client 还没生成或DATABASE_URL格式错误先执行pnpm prisma generate再检查.env中的连接串P1001: Cant reach database server数据库地址、端口或网络不通用数据库客户端先手动连接同串地址排除网络问题Error: tRPC failed或Unexpected token前端把服务端代码引入了客户端 bundle搜索是否在组件里 import 了server/*内容Type x is not assignable to type yz.input 校验类型与 Prisma 返回类型不一致优先使用z.string()等基础类型避免与数据库字段强耦合React hydration 报错服务端渲染与客户端首次渲染的 DOM 不一致检查是否直接渲染了Date对象需要序列化后再输出NEXTAUTH_SECRET missing生产环境没设置密钥在平台后台配置环境变量不要依赖.env文件4.2 我踩过的最深三个坑第一个坑Prisma 模型字段使用DateTime类型前端直接渲染。服务端返回一个Date对象经过 tRPC JSON 序列化后变成了 ISO 字符串但如果服务端组件直接拿到这个Date传给客户端组件React 的 hydration 就会因为时间格式不一致而疯狂报警。解决办法很简单在渲染前统一做一次toISOString()或者用String(date)把值转成字符串再传。第二个坑在 Next.js 的中间件里使用 Prisma。中间件默认运行在 Edge Runtime不支持 Node 原生模块Prisma Client 直接使用会报错。解决方法是让中间件只做 token 校验和路径重写凡是需要读数据库的逻辑都放到 API Route 或 Server Action 里。这也是我在初期反复踩坑后才明白的。第三个坑把 tRPC Router 当作“万能 API”把 HTTP 层之外的东西全塞进 procedure。比如有人会在 mutation 里直接写复杂的事务逻辑导致 procedure 动辄几百行。调试起来非常痛苦。我的经验是tRPC procedure 只做参数校验、鉴权判断和调用领域服务真正的业务逻辑放在src/server/services下面。这样 t3code 默认的链路才不会被滥用成巨无霸。4.3 包体积与构建性能优化经验t3code 模板默认集成了不少依赖但并不意味着初次打包就会很大。只要按照规则隔离客户端和服务端代码tree-shaking能正常工作。我习惯在package.json里加上依赖分析脚本analyze: ANALYZEtrue next build用next/bundle-analyzer看一下各模块体积。经验值上空模板的客户端 JavaScript 体积大约在 60-80 KB 左右这个数字在可接受范围。如果你发现自己项目首屏超过了 200 KB通常是下列原因之一直接把整个 tRPC Router 引入了客户端、在客户端组件里 import 了服务端依赖、没有使用 Next.js 的路由级代码分割。另外一个容易被忽视的点Prisma Client 体积偏大但它在服务端运行不会影响客户端 bundle。真正要小心的是一些常见的工具库比如dayjs、lodash如果只在服务端使用就不要在客户端组件顶层 import。要么按需引入要么让同一个工具库只在必要的一侧使用。t3code 的构建优化其实没有特别神秘的地方核心逻辑就是“把服务端的东西留在服务端把客户端的东西压缩到最小”。理解了这一点你看到模板里那些.server和.client后缀的文件就会明白它们不是形式主义而是为了帮助打包器区分边界。最后分享两个我自己的扩展建议如果你用 t3code 不是为了学习而是想实际交付产品我强烈建议你在拿到模板后做的第一件事不是加功能而是写一个真实的业务模块比如“用户上传头像”或者“发布带标签的文章”。因为只有把文件上传、多表关联、鉴权这些真实场景走一遍你才能理解哪些地方需要改、哪些地方要沿用模板默认策略。另一个建议是版本化你的 t3code 模板。我自己的做法是把它放到 Git 仓库里每次项目结束时把那些“反复写到同一遍”的代码抽象回模板中。时间久了这个工具会越来越像你自己的私有开发规范而不是一个固化的样板。毕竟工具的意义不是让你被规则绑定而是替你把重复劳动扛下来让你把精力放到真正值得想的业务问题上。