T3 Stack实战:用t3code搭建类型安全的全栈TypeScript应用

发布时间:2026/8/30 11:12:08
T3 Stack实战:用t3code搭建类型安全的全栈TypeScript应用 之前为了给团队快速搭建一个全栈 TypeScript 项目前后端类型割裂、数据校验重复、数据库建模不统一这些问题来回折腾了不少时间。后来接触到 T3 Stack 生态下的 t3code 这套代码生成与模板工程方案整个开发链路清爽了很多。本文就把这套从初始化到落地的完整流程拆解出来包含环境准备、项目结构、核心原理、完整实战案例和常见问题排查适合想了解 T3 技术栈、准备用 Next.js tRPC Prisma 搭建全栈应用的开发者新手也能一步步跟上。1. 背景T3 Stack 与 t3code 要解决的问题1.1 全栈开发中的类型割裂问题在传统的前后端分离开发中前端通常通过 REST API 与后端通信。前端用 TypeScript后端用另一种语言或框架时接口的请求参数和响应结构往往靠手写类型定义来维护。时间一长接口文档和实际返回不一致、前端类型定义落后于后端字段变更这类问题会反复消耗开发时间。即使是前后端都用 TypeScript 的项目如果前端通过 fetch 或 axios 手动调用接口类型推断也只会停留在Promiseunknown或某个手写的 interface 上。接口字段改了编译器不会主动提示只有运行时报错才能发现。t3code 所代表的并不仅仅是一个脚手架工具它背后是一整套“类型安全优先”的全栈开发思路让前端调用后端接口的整个链路都有类型约束从数据库表结构到服务端路由再到前端组件里的调用代码始终共享同一套类型定义。1.2 什么是 T3 StackT3 Stack 是由社区提出的一套全栈 TypeScript 技术组合核心成员包括技术作用解决的问题Next.jsReact 全栈框架服务端渲染、路由、API 能力TypeScript静态类型语言提供编译期类型检查tRPC类型安全 RPC 框架前后端 API 类型自动推导PrismaORM 与数据库工具数据库建模、迁移、类型生成Tailwind CSS原子化 CSS 框架快速开发 UI 样式其中 tRPC 是 T3 Stack 的关键它让前端直接调用后端函数而不需要手写 REST 路由和对应的类型声明。你只需要在后端写一个 router前端就能自动获得带完整类型提示的调用方法。1.3 t3code 在生态中的定位t3code 可以理解为 T3 Stack 生态中的代码生成与工程化方案。它把 Next.js、tRPC、Prisma、Tailwind 组合成一套可复制、可落地的项目模板开发者通过命令初始化项目后就能直接进入业务开发而不用从零搭建配置。当你使用npm create t3-applatest创建项目时得到的产物实际上就是 t3code 思路下的默认工程骨架。这个骨架里已经完成了TypeScript 严格配置。tRPC 服务端与客户端接入。Prisma 数据库配置。Tailwind 样式体系。环境变量模板。基础目录结构拆分。这也是为什么掌握 t3code 的初始化产物的结构比单纯敲几条命令更有价值你需要在产物的基础上继续开发而不是初始化完就结束。1.4 为什么要掌握这套方案从学习角度来说T3 Stack 能让你在同一个项目里体验 TypeScript 全栈开发的最佳实践从工程落地角度来说它减少了前后端联调成本让接口变更的反馈周期从“等到运行时”变成“编译期直接提示”。如果你准备学习全栈 TypeScript、想了解 tRPC 的类型推导机制、或者需要在团队里快速搭建一套类型安全项目骨架这篇文章都适合你跟练一遍。2. 环境准备与版本说明2.1 本地环境要求在开始之前先确认你的开发环境满足以下条件依赖建议版本说明Node.js18.x 及以上T3 项目需要较新的 Node.js 运行时npm9.x 及以上也可以用 pnpm 或 yarnpnpm可选8.x 及以上create-t3-app 支持多种包管理器VS Code最新稳定版配合 TypeScript 插件体验最好版本需要根据你的项目实际情况调整。如果你本地的 Node.js 版本过低某些依赖可能无法安装或运行时报错。检查 Node.js 版本node -v npm -v如果版本过低建议先升级 Node.js 再继续。由于不同系统升级方式不同这里不展开具体步骤安装完成后重新打开终端即可。2.2 数据库环境本文的实战案例使用 SQLite原因是不需要额外安装数据库服务。配置文件简单适合学习。create-t3-app 初始化时默认支持 SQLite。如果后面要切换到 PostgreSQL 或 MySQL只需要修改DATABASE_URL再调整少量 Prisma 配置即可。2.3 初始化命令说明npm create t3-applatest是 T3 社区提供的官方脚手架命令。它会引导你选择需要的功能模块然后生成项目骨架。接下来的步骤里我以示例项目名my-t3-app演示。你可以根据需要修改项目名但请注意项目名会影响 package.json 中的name字段和目录名。在终端中执行npm create t3-applatest my-t3-app如果你使用 pnpmpnpm create t3-applatest my-t3-app2.4 初始化交互选项执行命令后命令行会弹出交互式选项。参考选择如下选项建议选择说明TypeScript是T3 Stack 的基石必须启用Tailwind CSS是本文案例样式依赖 TailwindtRPC是核心类型安全 RPC 框架Prisma是数据库 ORM案例的数据操作NextAuth.js否本案例不需要登录鉴权后续可自行研究App Router是create-t3-app 新版默认如果你用的 create-t3-app 版本较新交互界面可能还会询问包管理器选择按你本地的工具选择即可。2.5 项目基础结构初始化完成后进入项目目录并安装依赖cd my-t3-app npm install启动开发服务器npm run dev访问http://localhost:3000能看到一个基于 Tailwind 的默认页面说明项目初始化成功。生成的项目结构主要包含以下目录my-t3-app/ ├── prisma/ │ └── schema.prisma ├── public/ ├── src/ │ ├── app/ # Next.js App Router │ │ ├── _components/ │ │ ├── api/ │ │ ├── layout.tsx │ │ └── page.tsx │ ├── server/ │ │ ├── api/ │ │ │ ├── routers/ │ │ │ ├── root.ts │ │ │ └── trpc.ts │ │ └── db.ts │ ├── trpc/ │ │ ├── react.tsx │ │ └── server.ts │ └── env.js ├── .env ├── .env.example ├── next.config.mjs ├── tailwind.config.ts └── tsconfig.json这个结构不是随意划分的每个目录都有明确职责本文后面会逐个说明关键文件的作用。3. 核心原理拆解类型安全全链路3.1 tRPC 的类型推导流程tRPC 的核心设计是服务端定义 router 和 procedure前端通过类型引用拿到Router类型然后 tRPC Client 就具备了完整的输入输出类型提示。为了便于理解我们把整个调用流程拆成几个环节前端组件调用 api.todo.list.useQuery() ↓ tRPC Client 发起请求 ↓ 服务端 todoRouter 的 list 函数执行 ↓ Prisma 读取数据库 ↓ 返回数据经过类型检查再回到前端关键点在于tRPC 的输入和输出类型是从服务端 router 里自动推导的。你在服务端给 procedure 加了一个zod输入校验前端调用时就能看到对应的参数类型你改了返回字段前端的类型提示也会同步变化。3.2 Zod 输入校验的作用在 tRPC 中给 procedure 添加input时通常配合 zod。import { z } from zod; export const todoRouter createTRPCRouter({ create: publicProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title }, }); }), });zod 在这里承担两个职责运行时校验请求进入 procedure 前先校验参数是否符合规则不符合直接报错。类型推导zod schema 能推出 TypeScript 类型这个类型会传递给前端调用方。所以你只需要在服务端维护一份 schema前端就能获得对应的输入类型。不需要再写一套前端接口类型定义。3.3 Prisma 类型与数据库的联动Prisma 会根据schema.prisma里的数据模型自动生成 TypeScript 类型。比如你定义了一个Todo模型Prisma Client 类型中就有一整套Todo相关的 create、findMany、update、delete 方法及参数类型。当你通过 tRPC 返回ctx.db.todo.findMany()的结果时返回类型自动携带Todo[]类型前端 useQuery 拿到的data也就能推断出对应的字段结构。这个联动链路的路径是prisma/schema.prisma ↓ prisma generate ↓ Prisma Client 类型 ↓ tRPC router 返回类型 ↓ 前端 useQuery 类型整个链路中类型没有被手写复制而是自动推导这就是 T3 Stack 能减少类型割裂问题的根本原因。3.4 Next.js App Router 与 tRPC 的集成方式create-t3-app 生成的模板中src/trpc/react.tsx负责创建 React 侧的 tRPC 客户端src/trpc/server.ts负责服务端组件调用 tRPC 时的客户端实例。在客户端组件中可以直接使用api这个对象use client; import { api } from ~/trpc/react; export function TodoList() { const { data: todos } api.todo.list.useQuery(); return div{JSON.stringify(todos)}/div; }在服务端组件中可以通过api服务端客户端直接查询import { api } from ~/trpc/server; export default async function HomePage() { const todos await api.todo.list(); return div{todos.length}/div; }两种方式都保留类型提示区别在于请求发起的位置不同。客户端组件在浏览器执行服务端组件在服务端执行。4. 完整实战案例搭建一个待办事项应用接下来我们用一个待办事项应用把 t3code 初始化产物从默认页面改造成一个可用的全栈功能模块。案例中会覆盖Prisma 数据模型定义。数据库表结构同步。tRPC router 编写。前端页面调用。运行验证。4.1 定义 Prisma 数据模型首先打开prisma/schema.prisma。默认模板中会有一个示例模型具体内容因 create-t3-app 版本而异这里我们直接使用下面的模型替换。// prisma/schema.prisma generator client { provider prisma-client-js } datasource db { provider sqlite url env(DATABASE_URL) } model Todo { id String id default(cuid()) title String completed Boolean default(false) createdAt DateTime default(now()) updatedAt DateTime updatedAt }字段说明id字符串类型主键使用cuid()生成全局唯一 ID。title待办事项标题必填。completed是否完成默认false。createdAt创建时间默认当前时间。updatedAt更新时间更新记录时自动维护。保存文件后执行 Prisma 的迁移或 push 命令将模型同步到数据库。npx prisma db push执行成功后Prisma 会生成对应的 Client 类型并基于 SQLite 创建dev.db数据库文件。由于我们改了 schema需要重新生成 Prisma Clientnpx prisma generateprisma db push适合开发阶段快速同步表结构生产环境建议使用prisma migrate dev生成迁移记录以保持表结构变更可追踪。4.2 检查环境变量项目根目录下有一个.env文件默认内容类似DATABASE_URLfile:./db.sqlite确保这一行存在并且对应prisma/schema.prisma中env(DATABASE_URL)的引用。如果你使用的是其他数据库比如 PostgreSQLURL 格式会不一样需要按实际环境调整。注意.env文件不应该提交到 Gitcreate-t3-app 已经默认在.gitignore里忽略它。新增的环境变量也要记得同步到.env.example方便其他开发者复制配置。4.3 编写 tRPC Router打开src/server/api/routers/目录。create-t3-app 默认会生成一个示例 router 文件我们新增一个todo.ts。// src/server/api/routers/todo.ts import { 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 }, }); }), create: publicProcedure .input(z.object({ title: z.string().min(1) })) .mutation(async ({ ctx, input }) { return ctx.db.todo.create({ data: { title: input.title }, }); }), toggle: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const todo await ctx.db.todo.findUnique({ where: { id: input.id }, }); if (!todo) { throw new Error(Todo not found); } return ctx.db.todo.update({ where: { id: input.id }, data: { completed: !todo.completed }, }); }), remove: publicProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { return ctx.db.todo.delete({ where: { id: input.id }, }); }), });代码说明list查询全部待办事项按创建时间倒序。query表示查询操作。create接收{ title: string }先由 zod 校验非空再写入数据库。mutation表示写操作。toggle根据 id 查出当前待办取反completed后更新。remove根据 id 删除记录。然后需要把这个 router 挂载到根 router 上。打开src/server/api/root.ts也可能是src/server/api/root.ts附近类似结构将todoRouter加入// src/server/api/root.ts import { todoRouter } from ~/server/api/routers/todo; import { createTRPCRouter } from ~/server/api/trpc; export const appRouter createTRPCRouter({ todo: todoRouter, }); export type AppRouter typeof appRouter;如果你在 create-t3-app 生成的模板中看到exampleRouter或其他示例 router可以直接删除或保留这取决于你的项目需求。这里我们用todo: todoRouter作为根路由下的命名空间。4.4 编写前端页面前端页面依赖 tRPC 客户端注入。create-t3-app 模板已经在src/app/layout.tsx的 Provider 配置中完成了相关接入正常情况下无需重复配置。先修改首页src/app/page.tsx// src/app/page.tsx import { TodoList } from ~/app/_components/todo-list; export default function HomePage() { return ( main classNamemx-auto max-w-xl py-10 h1 classNametext-2xl font-bold我的待办事项/h1 p classNamemt-2 text-sm text-gray-500 这是基于 T3 Stack 的待办事项示例 /p div classNamemt-6 TodoList / /div /main ); }这里我把页面改成了一个简单的容器真正的逻辑在TodoList组件里。创建src/app/_components/todo-list.tsx// src/app/_components/todo-list.tsx use client; import { useState } from react; import { api } from ~/trpc/react; export function TodoList() { const utils api.useUtils(); const [title, setTitle] useState(); const { data: todos, isLoading } api.todo.list.useQuery(); const createTodo api.todo.create.useMutation({ onSuccess: async () { await utils.todo.list.invalidate(); setTitle(); }, }); const toggleTodo api.todo.toggle.useMutation({ onSuccess: () { void utils.todo.list.invalidate(); }, }); const removeTodo api.todo.remove.useMutation({ onSuccess: () { void utils.todo.list.invalidate(); }, }); if (isLoading) { return p加载中.../p; } return ( div form classNameflex gap-2 onSubmit{(e) { e.preventDefault(); if (title.trim()) { createTodo.mutate({ title: title.trim() }); } }} input classNameflex-1 rounded-md border border-gray-300 px-3 py-2 value{title} onChange{(e) setTitle(e.target.value)} placeholder输入待办事项 / button classNamerounded-md bg-slate-800 px-4 py-2 text-white typesubmit 添加 /button /form ul classNamemt-4 space-y-2 {todos?.map((todo) ( li key{todo.id} classNameflex items-center justify-between gap-2 rounded-md border border-gray-200 px-3 py-2 label classNameflex items-center gap-2 input typecheckbox checked{todo.completed} onChange{() toggleTodo.mutate({ id: todo.id })} / span className{ todo.completed ? line-through opacity-60 : } {todo.title} /span /label button classNametext-sm text-red-500 onClick{() removeTodo.mutate({ id: todo.id })} 删除 /button /li ))} /ul {todos?.length 0 ( p classNamemt-4 text-center text-sm text-gray-400 暂无待办事项 /p )} /div ); }组件说明api.useUtils()获取 tRPC 工具的实例用于手动刷新查询缓存。api.todo.list.useQuery()调用服务端todo.list获取数据返回isLoading、data。api.todo.create.useMutation()前端调用 mutation成功后通过invalidate()让列表重新拉取。toggleTodo和removeTodo同理只有在服务端成功返回后才刷新列表。这里采用了“mutation 成功后刷新列表”的策略。简单业务这样可以复杂场景可以改用 tRPC 乐观更新来提升交互体验后文最佳实践里会提。4.5 运行与验证启动开发服务器npm run dev访问http://localhost:3000你会看到“我的待办事项”页面。操作流程输入框输入“学习 t3code”。点击“添加”按钮。列表中出现这条待办事项。点击复选框文字出现删除线表示已完成。点击“删除”按钮记录被移除。此时打开数据库也能看到数据确实写入了 SQLite。可以借助 Prisma Studio 可视化查看npx prisma studioPrisma Studio 会启动一个本地管理界面默认地址是http://localhost:5555。里面可以直接查看Todo表的数据也可以手动新增和修改记录方便开发期调试。4.6 预期效果说明当你完成上述步骤后整个待办事项应用已经具备完整的增删改查能力。更重要的是api.todo下的所有方法都带有完整的类型提示调用createTodo.mutate时参数会被约束为{ title: string }。读取todos时每一项都能推断出{ id, title, completed, createdAt, updatedAt }完整结构。如果你在服务端给title加了max(20)的校验前端输入超长会在运行时被拦截。这种体验正是 t3code 方案最有价值的地方一套代码写到底类型从前到后全程可用。5. 常见问题与排查思路5.1 常见问题表格问题现象常见原因解决思路DATABASE_URL找不到.env文件缺失或变量名不一致检查.env是否存在确认与 Prisma schema 引用一致prisma db push报错数据库文件被占用或 schema 语法错误关闭 Prisma Studio执行npx prisma validate检查 schema前端调用api.todo.create报类型错误tRPC Router 没有正确挂载到 root检查root.ts是否引入了todoRouter页面显示“加载中”一直不结束tRPC 请求失败后端报错打开浏览器控制台和终端日志查看具体报错信息npx prisma generate后类型没更新组件或 router 没有重新编译重启npm run dev必要时删除.next目录SQLite 数据写入失败数据库表结构未同步执行npx prisma db push或prisma migrate dev5.2 问题 1prisma 命令提示找不到数据库在执行npx prisma db push时如果报错提示找不到DATABASE_URL最可能的原因是.env文件不存在或者.env中的变量名与prisma/schema.prisma不一致。检查顺序项目根目录是否存在.env。prisma/schema.prisma中url env(DATABASE_URL)的变量名。.env中的DATABASE_URL是否配置了完整连接串。对于 SQLite正确格式是DATABASE_URLfile:./db.sqlite注意不要加协议前缀比如sqlite://这是常见配置错误之一。5.3 问题 2tRPC 前端调用方法没有类型提示如果你在前端组件中写api.todo.create时没有自动提示先检查src/server/api/root.ts是否导出了AppRouter类型。src/trpc/react.tsx或客户端 Provider 是否引用了AppRouter。修改 router 后是否重启开发服务器。tRPC 的类型推导依赖 TypeScript 的强类型引用。如果你在root.ts里引入了 router 却没有重新导出类型前端会失去类型来源。5.4 问题 3useQuery 数据刷新不及时mutation 执行成功后列表没有自动更新。原因通常是invalidate没有触发或者 invalidate 的目标 key 写错了。推荐做法const createTodo api.todo.create.useMutation({ onSuccess: async () { await utils.todo.list.invalidate(); }, });utils.todo.list.invalidate()表示让todo.list这个查询重新执行。如果你有多个查询都依赖同一个数据源可以考虑用utils.todo.invalidate()清理整个todorouter 下的缓存。5.5 问题 4生产构建时报类型错误npm run build时 TypeScript 检查报错但开发环境正常。常见原因是开发时浏览器缓存了旧的类型数据或者某个组件的类型推断存在问题。解决方式删除.next目录。重新执行npm run build。根据报错信息逐一修正类型问题。T3 项目默认 TypeScript 配置比较严格类型错误会在构建阶段直接暴露这是好事而不是问题。保持类型安全是 t3code 方案的核心价值之一。6. 最佳实践与工程建议6.1 环境变量管理所有敏感配置都放在.env中并且不要提交到 Git。在团队协作中.env.example需要保持更新# .env.example DATABASE_URLfile:./db.sqlite新成员克隆项目后复制一份.env.example为.env填入本地配置即可运行。不要在代码里写死数据库连接串也不要把生产环境的密钥放进next.config.mjs。6.2 Prisma 迁移策略开发阶段使用prisma db push快速同步表结构很方便但生产环境更推荐使用迁移npx prisma migrate dev --name add_todo_model迁移文件会被记录下来后续部署时执行npx prisma migrate deploy这样数据库结构变更可以被追溯也方便多环境保持一致。6.3 tRPC Router 拆分当业务复杂后不要把大量 procedure 都塞进同一个文件。建议按照业务模块拆分 routersrc/server/api/routers/ ├── todo.ts ├── user.ts ├── auth.ts └── order.ts然后在root.ts统一挂载export const appRouter createTRPCRouter({ todo: todoRouter, user: userRouter, auth: authRouter, order: orderRouter, });每个 router 只负责自己领域的数据读写职责清晰维护起来也方便。6.4 错误处理与日志tRPC procedure 内抛出的错误会被统一包装。你可以在src/server/api/trpc.ts中配置错误格式化器把错误转换成更友好的结构。不过在实际项目中要区分“客户端输入错误”和“服务端异常”客户端输入问题尽量用 zod 校验在输入阶段拦截。服务端内部错误要打日志并返回笼统的错误信息避免泄露实现细节。涉及数据库写入操作时考虑事务保证数据一致性。6.5 性能与缓存对于查询类的 procedure如果返回结果不频繁变化可以结合 React Query 的staleTime来控制请求频率api.todo.list.useQuery(undefined, { staleTime: 5_000, });这样 5 秒内多个组件触发同一查询时不会重复请求 tRPC 接口减少不必要的数据库查询。对于 mutation 的列表刷新如果列表很大优先使用乐观更新来提升体验const createTodo api.todo.create.useMutation({ onMutate: async ({ title }) { await utils.todo.list.cancel(); const prev utils.todo.list.getData(); utils.todo.list.setData(undefined, (old) [ { id: temp-id, title, completed: false, createdAt: new Date(), updatedAt: new Date(), }, ...(old ?? []), ]); return { prev }; }, onError: (_err, _input, ctx) { utils.todo.list.setData(undefined, ctx?.prev); }, onSettled: () { void utils.todo.list.invalidate(); }, });乐观更新会让界面先显示临时数据请求失败时再回滚交互会更流畅。6.6 安全边界与数据校验tRPC 提供了输入校验能力但不要只依赖前端页面。所有输入都需要在服务端经过 zod 校验包括字符串长度。枚举取值范围。权限校验。资源归属校验。例如更新待办事项时不能只校验id是字符串还要确认这个待办事项当前用户是否有权操作。在团队项目中该逻辑应该在 procedure 内实现并配合中间件统一管理权限。6.7 生产部署注意点使用 Next.js 部署时需要注意生产环境数据库连接串不要使用 SQLite 文件路径建议使用托管数据库。迁移命令要在应用启动前执行保证表结构就绪。DATABASE_URL和密钥通过环境变量注入不要写进容器镜像。构建产物只包含必要文件数据库文件不应该被打包。涉及到删除或批量更新操作时先备份数据再在测试环境验证 SQL 和参数。以最简的 Node.js 部署方式为例构建命令和启动命令通常是{ scripts: { build: next build, start: next start } }部署平台会执行npm run build然后运行npm run start。数据库迁移命令需要放在启动前执行npx prisma migrate deploy npm run start不同平台对启动命令的处理不同请按实际部署平台调整。7. 总结与下一步学习方向这篇教程围绕 t3code 所代表的 T3 Stack 工程化思路从概念、环境到初始化项目再到类型安全全链路拆解和待办事项实战完整走通了一个全栈 TypeScript 应用的开发流程。你不仅能启动项目还理解了 tRPC 的自动类型推导如何与 Prisma 模型联动也知道了 mutation 后如何刷新数据、构建时如何排查类型错误。接下来可以继续深入的方向包括掌握 Next.js App Router 的缓存与数据获取机制进一步优化 tRPC 与服务端组件的配合。学习 tRPC 中间件实现登录鉴权和权限控制。研究 Prisma 的迁移工作流理解db push与migrate在开发和生产环境中的不同用法。将数据库从 SQLite 切换到 PostgreSQL感受连接串和环境变量管理的变化。如果想做更大型项目可以在此基础上引入 NextAuth.js完成用户认证闭环。实际项目从开发到上线优先关注三件事数据库迁移是否在部署前执行、敏感配置是否通过环境变量隔离、服务端输入校验是否完善。把这三个风险点控制住T3 技术栈的体验会非常稳定。如果你在搭建过程中卡在某个报错回到第 5 节的排查表格按顺序逐项检查。动手把待办事项案例跑通再往里面加自己的业务模块比单纯读代码理解得更快。