前两天把一个攒了很久的 side project 用 t3code 重新搭了一版整个过程比我预想中顺不少。顺手把 t3code 的使用心得和实现细节整理成文给打算从零开始做全栈应用、又不想在脚手架配置上浪费太多时间的同学做个参考。t3code 是一个基于 T3 技术栈的全栈应用脚手架它把 Next.js、TypeScript、Tailwind CSS、Prisma 和 tRPC 打包成一套开箱即用的项目模板。只要执行一条初始化命令就能得到一个前后端完全打通、并且从数据库到组件都共享同一套类型定义的应用骨架。t3code 解决的核心问题是“全栈类型安全”和“项目启动速度”这两件事。适合已经会写 React、但对服务端和数据库配置还不够熟的开发者也适合想快速验证产品想法、又不想被一堆基建问题拖住的人。这篇文章我会从为什么需要 t3code、怎么上手、核心架构、具体做一个带登录的 Todo 功能再到常见的坑和部署一条线讲完。涉及的命令和代码我都直接贴出来你可以跟着操作。1. 项目定位为什么会有 t3code以及它到底帮你省了哪些事1.1 T3 技术栈的核心理念T3 并不是某个单一框架的名字而是一组“类型安全优先、务实渐进”的技术组合。通常指的是 Next.js TypeScript Tailwind CSS tRPC再搭配 Prisma 和 NextAuth.js。这个组合的核心主张是所有端到端的数据交互都通过类型系统保证而不是靠运行时猜测。“类型安全优先”听起来像口号我举一个实际例子。以前写前后端联调你需要自己在前端定义一个 interface 去“冒充”后端返回的数据结构。一旦后端接口改了字段名前端编译期不会报错只有页面运行时才会出现undefined或者白屏。而在 t3code 生成的工程里从数据库表定义开始就自动生成类型所有链路共享同一份类型推导。说人话数据库里有一个title字段你在前端组件里误写成todo.titile编译时直接报错根本等不到运行时。所以 T3 技术栈的本质是把“类型安全”当作一项基础设施来对待。t3code 不是发明了新技术而是把这套已经被验证过的组件组合起来做成了可复用的模板。你不需要每次都从零配置只要理解每个模块的位置和作用就能快速上手。1.2 t3code 解决的三个痛点第一选型困难症。我们团队以前新起项目光技术讨论就要半天用 Pages Router 还是 App Router用 Redux 还是 React Query用 axios 还是 fetch用 TypeORM 还是 Prisma。t3code 直接给出了一套经过验证的默认组合。如果某些模块用不上初始化时也可以关掉而不是被迫接受一个“全家桶”。第二配置地狱。NextAuth 要配回调地址Prisma 要配数据库连接Tailwind 要配 content 路径这些零碎配置第一次做很容易漏。t3code 把这些预置好并且保证它们能协同工作。比如 Prisma 的db.ts里会做全局单例避免开发环境热更新时创建太多数据库连接tRPC 的 context 会自动注入 session省去自己写接口鉴权的重复劳动。第三前后端类型不同步。只要经历过一次后端字段改名导致前端静默出错的痛苦你就能理解 tRPC 的价值。t3code 把 tRPC 集成到 Next.js 的 App Router 里前端调用.useQuery()时返回数据的类型是自动推导出来的不需要手写第二个 interface。改后端模型时前端所有引用点都会被 TypeScript 编译器标红你可以一次性改完而不是上线后靠用户反馈来发现问题。2. 快速上手5 分钟跑通一个 t3code 初始项目2.1 环境准备开始之前需要确认本机环境满足下面这几点Node.js 版本 18.17 以上。t3code 底层依赖 Next.js 14 和 TypeScript 5太旧的 Node 跑不起来。推荐使用 pnpm 作为包管理器。pnpm 的依赖管理更严格t3code 内部的一些 workspace 配置默认按 pnpm 来。没有装的话先用npm install -g pnpm装一个。Git 已经安装后续初始化流程会用到。用终端检查一下node -v pnpm -v如果 Node 版本太低建议先装一个 nvm方便切换版本。t3code 本身不强制要求特定数据库本地开发用的是 SQLite所以不需要额外安装数据库服务这对快速体验非常友好。2.2 初始化命令与交互选项在终端执行npx create-t3codelatest my-app或者使用 pnpmpnpm create t3codelatest my-app执行后会进入交互式问答。第一次跑的时候可能会被大量选项搞懵我把每一项拆开来说Next.js App Router / Pages Router建议选 App Router。它是 Next.js 新版本的主推支持 Server Components配合 tRPC 时 t3code 已经帮你处理好了边界。Prisma默认开启。它让你的数据库建模和查询都获得类型安全。本地默认使用 SQLite几乎零成本。NextAuth.js认证库。你的应用如果需要用户登录就保留它。它会自动生成一个包含getServerSession的 auth 封装。tRPC建议保留。这是 t3code 的灵魂所在少了它前后端类型共享就无从谈起。Tailwind CSS样式方案。如果你习惯用普通 CSS 也可以关闭但默认带着能省很多事。ESLint / Prettier默认开启没什么坏处。我的建议是新手第一次全选先拿到一套完整可运行的项目再根据需求裁剪。我自己的项目全选了后面并没有觉得冗余反而每个模块都用上了。2.3 首次启动与验证初始化完成后进入项目目录cd my-app pnpm install npx prisma db push npx prisma generate pnpm devprisma db push会把schema.prisma里定义的模型同步到 SQLite 数据库文件prisma generate会生成 Prisma Client 类型。之后打开http://localhost:3000你应该能看到一个带登录链接的首页。如果之前没有配置任何 OAuthNextAuth 默认提供邮箱登录。开发模式下它会把登录链接直接打印在终端里点开即登录非常方便。此时就可以登录然后访问页面里受保护的示例接口比如/api/trpc/post.all。我第一次跑通时最直观的体验是前端调用 query 时返回数据的所有字段都有智能提示根本不需要查接口文档。3. 核心架构端到端类型安全是怎么实现的3.1 数据流全景t3code 之所以能做到端到端类型安全关键在一条完整的数据链路它的走向是这样的浏览器里的 React 组件 → 调用 tRPC 的 React Query Hooks如api.todo.getAll.useQuery() → 通过网络请求打到 Next.js 的 Route Handler/api/trpc/*→ tRPC 将请求路由到对应的 Procedure → Procedure 内部直接调用 Prisma Client 操作数据库 → 返回数据。整个过程不需要手写 REST 接口、不需要手写数据校验层、不需要手写枚举类型。tRPC 会自动推断 Procedure 的输入输出类型Prisma 会根据模型生成数据库查询的类型React 组件里的data字段就是数据库查询返回结果的精确类型。有一处类型错误编译器会立刻指出来而不是等接口跑起来才发现。3.2 目录结构与关键文件拆解初始化之后的目录结构大致是这样我删掉了一些无关文件prisma/ schema.prisma src/ app/ api/trpc/[trpc]/route.ts layout.tsx page.tsx _components/ server/ api/ root.ts routers/ post.ts auth.ts db.ts trpc/ react.tsx server.ts逐个说重点prisma/schema.prisma你的数据模型定义文件。所有业务表结构都写在这里。src/server/db.ts创建 Prisma Client 实例并做全局缓存。开发模式下 Next.js 的 HMR 会频繁刷新模块如果不做单例可能会导致数据库连接数暴涨。src/server/auth.tsNextAuth 的配置出口导出一个getServerAuthSession函数用于在服务端获取当前登录状态同时也能注入到 tRPC 的 context 中。src/server/api/root.tstRPC 的根路由文件里面把各个子 Router 合并成一个appRouter。src/server/api/routers/具体的业务 Router比如示例的post.ts。后续我们新增的 Todo 功能也会放这里。src/trpc/react.tsx前端 tRPC 客户端导出一个api对象包含所有 React Query Hooks以及api.useUtils()等操作缓存的方法。src/trpc/server.ts服务端调用 tRPC 的 Helper适合在 Server Component 里直接查询数据。src/app/api/trpc/[trpc]/route.ts所有 tRPC 请求的统一入口。Node.js 环境下的 HTTP 请求都会走这个 handler。3.3 tRPC 的两个核心概念Procedure 与 Middleware在 t3code 里tRPC 被封装得比较薄核心概念需要你搞明白。Procedure 是 tRPC 的基本操作单元。你可以理解为“一个后端函数”。它有query、mutation、subscription三种类型query用于取数据mutation用于写操作subscription用于实时数据。Router 就是一组 Procedure 的集合。Middleware 则是在 Procedure 执行前后可以挂载的逻辑。t3code 在src/server/api/trpc.ts里写了一个protectedProcedure它的作用就是校验用户是否已登录export const protectedProcedure t.procedure.use(({ ctx, next }) { if (!ctx.session?.user) { throw new TRPCError({ code: UNAUTHORIZED }); } return next({ ctx: { session: { ...ctx.session, user: ctx.session.user }, }, }); });每个需要登录才能访问的接口统一使用protectedProcedure而不是createTRPCProcedure从源头杜绝“忘了加鉴权”的问题。我第一次写后端接口时经常忘后来强制自己只用protectedProcedure一个月下来就形成了习惯。4. 实操用 t3code 快速实现一个带登录的 Todo 应用4.1 设计数据模型在prisma/schema.prisma里新增一个 Todo 模型model Todo { id String id default(cuid()) content String done Boolean default(false) userId String user User relation(fields: [userId], references: [id]) createdAt DateTime default(now()) updatedAt DateTime updatedAt }这里User模型是 NextAuth 自动生成的我们只需要在 User 模型上加一个反向关联字段model User { // 原有的 accounts, sessions 字段保持不变 todos Todo[] }保存后运行npx prisma db push npx prisma generate如果以后要改动模型并保留数据就需要用npx prisma migrate dev来生成迁移记录。db push更适合开发初期表结构还不稳定的阶段。4.2 编写 Todo Router在src/server/api/routers/下新建todo.tsimport { z } from zod; import { createTRPCRouter, protectedProcedure } from /server/api/trpc; import { TRPCError } from trpc/server; export const todoRouter createTRPCRouter({ getAll: protectedProcedure.query(({ ctx }) { return ctx.db.todo.findMany({ where: { userId: ctx.session.user.id }, orderBy: { createdAt: desc }, }); }), create: protectedProcedure .input(z.object({ content: z.string().min(1).max(500) })) .mutation(({ ctx, input }) { return ctx.db.todo.create({ data: { content: input.content, userId: ctx.session.user.id, }, }); }), toggle: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const todo await ctx.db.todo.findUnique({ where: { id: input.id }, }); if (!todo || todo.userId ! ctx.session.user.id) { throw new TRPCError({ code: NOT_FOUND }); } return ctx.db.todo.update({ where: { id: input.id }, data: { done: !todo.done }, }); }), delete: protectedProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) { const todo await ctx.db.todo.findUnique({ where: { id: input.id }, }); if (!todo || todo.userId ! ctx.session.user.id) { throw new TRPCError({ code: NOT_FOUND }); } return ctx.db.todo.delete({ where: { id: input.id } }); }), });然后在src/server/api/root.ts里把它加进去import { todoRouter } from ./routers/todo; export const appRouter createTRPCRouter({ post: postRouter, todo: todoRouter, });这里有一个非常容易忽略的重点在toggle和delete里我不仅检查了 Todo 是否存在还检查了todo.userId是否等于当前登录用户的 id。否则一个登录用户可以任意删除别人的 Todo这是很常见的安全漏洞。t3code 不会自动帮你做对象级权限判断必须自己在逻辑里补上。4.3 前端组件接入新建src/app/_components/TodoList.tsxuse client; import { useState } from react; import { api } from /trpc/react; export function TodoList() { const utils api.useUtils(); const [content, setContent] useState(); const { data: todos, isLoading } api.todo.getAll.useQuery(); const createMutation api.todo.create.useMutation({ onSuccess: () { void utils.todo.getAll.invalidate(); }, }); const toggleMutation api.todo.toggle.useMutation({ onSuccess: () { void utils.todo.getAll.invalidate(); }, }); const deleteMutation api.todo.delete.useMutation({ onSuccess: () { void utils.todo.getAll.invalidate(); }, }); if (isLoading) return div加载中.../div; return ( div classNamemax-w-md mx-auto p-4 form onSubmit{(e) { e.preventDefault(); if (!content.trim()) return; createMutation.mutate({ content }); setContent(); }} classNameflex gap-2 mb-4 input value{content} onChange{(e) setContent(e.target.value)} classNameflex-1 border rounded px-2 py-1 placeholder新 Todo / button typesubmit classNamebg-blue-500 text-white rounded px-3 py-1 添加 /button /form ul classNamespace-y-2 {todos?.map((todo) ( li key{todo.id} classNameflex gap-2 items-center input typecheckbox checked{todo.done} onChange{() toggleMutation.mutate({ id: todo.id })} / span className{todo.done ? line-through : }{todo.content}/span button classNameml-auto text-red-500 onClick{() deleteMutation.mutate({ id: todo.id })} 删除 /button /li ))} /ul /div ); }这里最能体现 t3code 的好处api.todo.create.useMutation()这种写法完全不需要自己维护缓存状态。每次 mutation 成功后我们调用utils.todo.getAll.invalidate()让列表重新请求一次React Query 会自动更新页面。整个过程中没有任何手工拼接 URL、序列化 JSON 的痕迹。4.4 在页面中挂载并控制登录态在src/app/page.tsx中我们可以在服务端判断登录状态只有登录用户才能看到 TodoListimport { auth } from /server/auth; import { TodoList } from ./_components/TodoList; export default async function HomePage() { const session await auth(); if (!session?.user) { return div classNametext-center mt-10请先登录/div; } return TodoList /; }因为TodoList用到了useState和 tRPC hooks必须把它拆成use client组件而page.tsx保持 Server Component。这个边界在 t3code 里已经提前划好你只需要遵循模式即可。此时启动项目登录后就能正常操作 Todo 了。5. 常见问题速查与排查实录5.1 Prisma Client 类型报错症状启动项目时 TS 报错Cannot find module prisma/client或者Property todo does not exist on type PrismaClient。原因改了schema.prisma之后没有运行prisma generate。Prisma Client 不是普通 npm 包它的类型和查询 API 是根据 schema 动态生成的。如果不重新生成代码里引用新模型就会报错。解决每次修改 schema 后执行npx prisma generate。更稳妥的方式是在package.json的postinstall脚本里加上prisma generate这样每次安装依赖后都会自动生成{ scripts: { postinstall: prisma generate } }5.2 tRPC Hooks 找不到 api 对象症状在客户端组件里使用api.todo.getAll.useQuery()时编辑器提示api为 undefined或者直接报Cannot find name api。原因没有从正确路径导入api或者组件没有被TRPCReactProvider包裹。t3code 中api定义在src/trpc/react.tsx并且layout.tsx里已经挂载了 Provider。如果你新增了一个页面但没有经过根 layoutProvider 就不会生效。解决检查src/app/layout.tsx是否包含TRPCReactProvider{children}/TRPCReactProvider。同时所有使用 hooks 的组件顶部必须有use client指令否则会在 Server 端执行出错。5.3 NextAuth 环境变量和回调地址症状点击登录后跳转出现[next-auth][error][MISSING_SECRET]或者第三方登录一直转圈。解决在.env中设置NEXTAUTH_SECRET。可以用系统自带命令生成openssl rand -base64 32如果使用 GitHub OAuth回调地址必须填http://localhost:3000/api/auth/callback/github。部署到生产环境后记得把应用域名填进去并重新生成安全的 secret。这个坑我踩过两次每次都是因为没仔细看 NextAuth 官方文档里“回调 URL 要和 baseUrl 保持一致”的提示。5.4 SQLite 数据库锁问题症状本地开发时偶尔出现database is locked错误。原因SQLite 不像 PostgreSQL、MySQL 那样适合高并发写入。多个进程同时写同一个dev.db文件或者一个连接长时间占用事务就可能锁库。解决开发阶段尽量保持只有一个pnpm dev进程在跑不要同时启动 dev server 和测试脚本。如果项目进入生产阶段建议把DATABASE_URL换成 PostgreSQL。t3code 的 Prisma schema 不用改只需要重跑prisma db push或migrate即可。6. 部署与后续扩展6.1 部署到 Vercel 的关键配置t3code 生成的 Next.js 项目非常适合部署到 Vercel但有几个点需要注意生产环境变量要全部配置在 Vercel 的项目设置里。包括DATABASE_URL、NEXTAUTH_SECRET以及 OAuth 相关的 key。构建指令建议设置为prisma generate next build。因为 Vercel 的构建环境是全新的必须重新生成 Prisma Client。如果你的后端 API 跑在 Serverless 函数里用 SQLite 会导致数据丢失。因为 Serverless 的函数实例是无状态的文件系统不持久化。必须使用托管数据库比如 PostgreSQL。使用 Prisma Serverless 时注意数据库连接数限制。最简单的优化是开启 Prisma Accelerate 连接池或者在连接串上启用 PgBouncer 兼容模式。6.2 从 t3code 项目继续延伸当你的 Todo 应用跑通后可以顺着 t3code 的架构继续加功能。比如给 Router 增加分页参数只需要在 input 里加一个page字段前端立刻获得类型提示。接入文件上传上传后的 URL 可以当成普通字符串存在 Prisma 模型里。把某些读接口改成procedure后部署到 Edge Runtime享受更低的访问延迟。如果项目变成 monorepo可以把 tRPC Router 拆成单独的 package让多个前端项目共用同一套类型。我个人在使用 t3code 半年后最大的体会是它并没有要求你学习什么新的哲学而是把早已成熟的技术组合用一套合理的默认配置串了起来。你不需要再为“该不该用 GraphQL”“要不要自己封装 fetch”之类的问题纠结。先跑起来再在架构上做局部调整这才是 t3code 最舒服的使用节奏。最后再分享一个小技巧如果你想把某个已有项目也迁移到 t3code 的体系里可以先用 t3code 初始化一个空白项目然后把原有组件和业务代码逐步搬进来。迁移过程中TypeScript 编译器会帮你标出所有类型不匹配的位置。我在一次历史项目重构中就是这样做的原本计划两周的迁移实际只用了四天大部分时间都花在改命名上而不是排查联调 bug。这种“类型牵引重构”的体验值得每个被接口文档折磨过的开发者试一次。
阅读完成 · 觉得有帮助?